Practical PHP Refactoring: Encapsulate Field
Join the DZone community and get the full member experience.
Join For FreeThe public field has been abandoned in modern OOP for a return to the origins (although the getter/setter by default solution is not much better.) An object encapsulate a state, and fields are part of its state; The same object exposes behavior via public methods.
This refactoring is about substituting a public field with a field with a stricter visibility, which is then accessed or mutated (if necessary) with other methods, not necessarily a getter and a setter.
Why should I hide public properties?
The basic alternative to a public field, getters and setters, allow indirection to be introduced in case it is necessary. For example, you can normalize a field's value, or transform it into a calculated field since the contract comprehends only a method ($object->getName()) and not the actual field ($this->name).
You can also omit a getter or a setter to obtain respectively a private field which is totally encapsulated, or a final field that is not modifiable once construction of the object is finished. There is no final keyword in PHP, although it copies much of Java's object model, which was the most diffused one when the new paradigm was introduced in PHP 5.
You may also need to only expose public methods and not fields for technical requirements. For example, Doctrine 2 lazy loading cannot intercept the access to public properties in order to execute a query, so in order to use the ORM with this technique you have to hide public fields.
Steps
- Create getters and setters. Just a getter, if the field is already valorized during construction. Otherwise, a getter and a setter to allow for modifications.
- Replace the assignments and readings of the field with calls to the getter or the setter. In case there are assignments but you do not intend to provide a setter, it's better to insert a @deprecated setField() method and to refactor later to eliminate it, once all the accesses to the field are passing from there.
- Check that the tests are still green - they should run as-is after the changes, apart from the same changes on usage and assignment. There is no large change in design introduced during this refactoring.
- Declare the field as private or protected. Unless you already have subclasses, or you intend to allow subclassing as a documented extension point, I would go with the private visibility.
In the PHP world there was a diffused trend of always providing protected fields, but not all classes should be (or are) used for subclassing, and for some objects there's not even the choice of substituting the originals in the framework's object graph. A quick look to Symfony 2 reveals that private is not a taboo anymore, while most of Zend Framework 2 consists of 1.x ported code and still have many protected fields around (it could be a design decision.)
Example
In the initial state, we have a Reservation class containing a public field. This field can be modified at any time, whenever a reference to a Reservation object is in the scope.
<?php class EncapsulateField extends PHPUnit_Framework_TestCase { public function testTheFieldCanBeManipulated() { $reservation = new Reservation(); $reservation->date = '2010-01-01'; $this->assertTrue($reservation->isOutdated()); } } class Reservation { public $date; public function isOutdated() { // global state! Avoid this in real code return $this->date < date('Y-m-d'); } }
We add just a setter; we don't need also a getter to make the client code work.
class Reservation { public $date; /** * @param string $date */ public function setDate($date) { $this->date = $date; } public function isOutdated() { // global state! Avoid this in real code return $this->date < date('Y-m-d'); } }
Now we change the client code, represented just by our unit test in this small example.
<?php class EncapsulateField extends PHPUnit_Framework_TestCase { public function testTheFieldCanBeManipulated() { $reservation = new Reservation(); $reservation->setDate('2010-01-01'); $this->assertTrue($reservation->isOutdated()); } }
Now we can safely restrict the visibility of $this->date to private and check that there are no direct references to the field outside, which will make the tests exercising them fail. PHP does not check member access in its on the fly compilation step: you need tests exercising all your code to make sure you're not accessing a variable that has become private.
class Reservation { private $date; /** * @param string $date */ public function setDate($date) { $this->date = $date; } public function isOutdated() { // global state! Avoid this in real code return $this->date < date('Y-m-d'); } }
Opinions expressed by DZone contributors are their own.
Comments