My development blog about PHP, Nette Framework, ElasticSearch or occasional short tips.
Recently I was thinking about OOP and how to use it properly in PHP. I spent hours of reading smart people opinions and hours writing my own view of using OOP in PHP. In this repository are all my thoughts. Data encapsulating is mandatory in true OOP design, you don't want to use raw data. And in this article i will show you how to encapsulate data.
Goal of this example is to demonstrate how to transform this simplified process of fetching, updating and saving to be as OOP as possible. Our source of data is ElasticSearch which returns array of data for entity. In application we will change name of entity and add data to nested array. Last step is saving changed entity to elastic as array with same structure as entity.
class Title
{
private $id;
private $name;
public function __construct($data)
{
$this->metadata = $data;
}
public function getName()
{
return $this->metadata['name'];
}
public function setName($name)
{
$this->metadata['name'] = $name;
}
}
$data = $this->elastic->get('3315342');
$title = new \Before\Model\Entity\Title($data);
$title->setName('Logan');
$title->setPeople([
123456 => [
'id' => 123456,
'name' => 'Hugh Jackman',
'character' => 'James Howlett',
],
]);
$title->getName();
foreach ($title->getPeople()->getData() as $person) {
$person->getName();
$person->getCharacter();
}
$this->elastic->save($entity->toArray());
This is working solution i was using for while but there are issues with maintainability, testing and code consistency. Those are things OOP should help with.
To achieve encapsulation we should move all data setting and validation to constructor.
class Title
{
public function __construct($data)
{
if ( ! isset($data['id'])) {
throw new \InvalidArgumentException();
}
if (is_string($data['id'])) {
throw new \InvalidArgumentException();
}
if ($data['id'] < 1) {
throw new \InvalidArgumentException();
}
if ($data['id'] > 9999999) {
throw new \InvalidArgumentException();
}
$this->id = $data['id'];
}
}
Now we have valid property id, but entity still has set methods to change its internal properties. So we need to duplicate validation to setter right? And what if we need 'id' validation in another object? Encapsulation is what we need. To avoid duplication of code we can encapsulate data to another class like this. source And bonus is constructor will be cleaner, more understandable and more maintainable. It won't be too long and it won't be doing too much things.
class Id
{
private $value;
public function __construct(
int $id
)
{
if ($id < 1) {
throw new \InvalidArgumentException();
}
if ($id > 9999999) {
throw new \InvalidArgumentException();
}
$this->value = $id;
}
public function value()
{
return $this->value;
}
}
class Title
{
public function __construct(Id $id)
{
$this->id = $id;
}
}
$title = new Title(
new Id($data['id'])
);
Now we have universal id validating class. We can go deeper as we can encapsulate id value into class 'integerType' to validate integer and use that validation in other places. In my example I went that far.
But this does not solve problem with setter completely, we can still manipulate Title entity properties during its lifetime. That means title entity is not immutable. Making objects immutable is our next step. Best solution is to drop setters and don't use them. It is very unlikely to change entity id. But you can have another properties which you might want to edit, like name. Still setter is not best solution as described many times. In this example i have rename() function. (source) This way entity Title stay valid at all times.
public function rename(\After\Model\Entity\Title\Name $name)
{
$this->name = $name;
}
Encapsulating, immutability and data validation might not have much sense when you are author of the data and it is from your database, but other people (including future you) will easily know how to construct entity, add/edit data. And desired entity will be valid at all times.
Also this will be really handy if you need to import data to your project from another system, no need for further data validation just build entity and you are ready to go.
Updated example of original application mentioned above. All data are encapsulated, validated, and objects are immutable. Changing name is done by dedicated typed function. And save function requires interface IEntity instead of array. More about transforming entity to array is in my example.
$data = $this->elastic->get('3315342');
$people = new \After\Model\Collection\People(
new \After\Model\Entity\Person(
new \After\Model\Entity\Person\Id(
new \After\Model\Entity\IntegerType(1772)
)
, new \After\Model\Entity\Person\Name(
new \After\Model\Entity\StringType('Patrick Stewart')
)
, new \After\Model\Entity\Person\Character(
new \After\Model\Entity\StringType('Charles Xavier')
)
)
);
$entity = new \After\Model\Entity\Title(
new \After\Model\Entity\Title\Id(
new \After\Model\Entity\StringType('asdfghjk')
)
, new \After\Model\Entity\Title\Name(
new \After\Model\Entity\StringType('Logan')
)
, new \After\Model\Entity\Title\Description(
new \After\Model\Entity\StringType('In the near future, a weary Logan cares for ...')
)
, new \After\Model\Entity\Year(
new \After\Model\Entity\IntegerType(2017)
)
, new \After\Model\Entity\Ids(
new \After\Model\Entity\Ids\Imdb(
new \After\Model\Entity\IntegerType(3315342)
)
)
, $people
);
$newName = new \After\Model\Entity\Title\Name(
new \After\Model\Entity\StringType('Old man Logan')
);
$entity->rename($newName);
$entity->people()->add(
new \After\Model\Entity\Person(
new \After\Model\Entity\Person\Id(new \After\Model\Entity\IntegerType(413168))
, new \After\Model\Entity\Person\Name(new \After\Model\Entity\StringType('Hugh Jackman'))
, new \After\Model\Entity\Person\Character(new \After\Model\Entity\StringType('Logan'))
)
);
$this->elastic->save($entity);