Is it good to set properties' values with setters inside the constructor?
In other words: is it ok to delegate to a setter when initializing a property in the constructor? The short answer is: It doesn't really matter. If there is no validation (other than the type) made, then there's really no point in it, but there's also no downside. If the is more validation to be made, then one could make the point that it keeps the constructor clean while reducing the amount of duplicate code. If there is additional checks to be made, then delegating to the setter for the common validation and implementing the additional one in the constructor will help keep the two workflows separate.
In the end, I don't think there's really a concern here. It's neither good nor bad practice to do it. Do what feels best for your specific use case.
Is it good to get their values inside class' methods with getters?
If you mean "should I access object properties through getters", then: Yes. A thousand times yes. Object properies should always be private and accessed through getters. Public properties violate one of the core concepts of Object-Oriented: Encapsulation. Whether or not each member has a getter and has logic or not is completely up to you and will depend on the use case.
if you mean "should I use the getter to access a propery from within the object" then it really doesn't matter and depends on the scenario. There may be times where using the public getter makes no practical sense and doesn't help the readability/maintainability of the code. There may be cases where it totally makes sense to call the public getter. For example when dealing with a function in the style of isFoo()
which returns true
or false
based on a couple of conditions, it makes sense to simply call isFoo()
instead of repeating the conditions again.
Where it would be better to set the default properties' values: explicitly initialize properties with them, set them as default arguments values (as it's done for now) or specify them in both places?
This one is a little more complex. In my mind there are three possible scenarios when choosing how to initialize an object property.
- The value MUST be passed to the constructor.
This is a case where a certain property must be specified when instantiating the object. If a value is not specified, the code should fail. For example, when creating an object of type User
, then it is probably safe to assume it requires a firstName
, lastName
and email
. If one or all of those variables are not passed to the constructor, the code should simply fail: the contract set by the constructor is not fulfilled.
class User {
private $firstName;
private $lastName;
private $email;
public function __construct(string $firstName, string $lastName, string $email) {
$this->firstName = $firstName;
$this->lastName = $lastName;
$this->email = $email;
}
}
$user = new User();
Exception: Argument 1 passed to User::__construct() must be of the type string, none given
- The value MAY be passed to the constructor.
This is a case where you want to allow a value to be set in the constructor, but is not required to instatiate the object. Taking the User
example from before, we could also have an middleName
property that a user can have, and we want to be able to set it the constructor.
class User {
private $firstName;
private $lastName;
private $middleName;
private $email;
public function __construct(string $firstName, string $lastName, string $email, $middleName = null) {
$this->firstName = $firstName;
$this->lastName = $lastName;
$this->email = $email;
$this->middleName = $middleName;
}
}
$user = new User('John', 'Doe', 'johndoe@email.com'); // does not throw an Exception
in my opinion there are very little reasons to actually implement a constructor in this way. It adds unnecessary complexity to the constructor while not actually solving any real problem. Sure it may be a quick and dirty way to add a parameter to the constructor without breaking any existing code, however if you are modifying the rules for creating an instance of an object, then the existing code should probably be updated to reflect this change.
- The value MUST be initialized to a default value.
This is a case where you want a certain property to be initialized to a specific default value as soon as the object is instantiated. This way there is no need to constantly check if the property has been initialized or not, or check for a null
value when dealing with this property. If a specific use case requires that value to be overwritten right away, simply call the setter after instantiating the object.
class User {
const USER_DEFAULT = 'default';
private $firstName;
private $email;
private $propertyToInitialize = self::USER_DEFAULT;
public function __construct(string $firstName, string $email) {
$this->firstName = $firstName;
$this->email = $email;
}
public function getInitializedProperty() {
return $this->propertyToInitialize;
}
}
$user = new User('John', 'johndoe@email.com');
echo $user->getInitializedProperty(); // outputs: 'default'
If you need a property's value to be part of a specific subset of values, I recommend checking out Enums