27
Revealing Module design pattern and when (not) to use it in PHP
One of the most common design patterns used in JavaScript is Revealing Module. Since JavaScript initially didn't have classic Java-like OOP system, Revealing Module concept was used instead to create simple objects and implement private-like object properties. Revealing Module also fits well into functional nature of JavaScript. (You can see example implementation of Revealing Module in JS on GitHub Gist.)
Since PHP includes OOP for very long time, it is quite non-idiomatic to replace classical OOP object handling with functional alternatives like Revealing Module. However thanks to great support of anonymous functions in PHP it is very straightforward to implement it. You can do it as an exercise to dig deeper into functional PHP programming or you can use it in your mainly procedural codebase which you don't want to mix with OOP paradigm, but still want to use some object-like approach.
For easier explanation, I will start with practical example - using Revealing Module in PHP to implement simple "rectangle" object. You will easily understand that $rectangle
is a constructor function, which returns $rectangle0
- an array representing newly created object instance with public properties and functions on it. Local variables inside $rectangle
functions on the other hand act like private members.
<?php
$rectangle = function (int $a, int $b) {
$name = "rectangle";
$is_square = function () use (&$a, &$b) {
return $a === $b;
};
$rectangle0 = [
"get_area" => function () use (&$a, &$b) {
return $a * $b;
},
"get_perimeter" => function () use (&$a, &$b, $is_square) {
return $is_square() ? (4 * $a) : (2 * ($a + $b));
},
"set_a" => function (int $a1) use (&$a) { $a = $a1; },
"set_b" => function (int $b1) use (&$b) { $b = $b1; },
"get_name" => fn() => $name,
"to_string" => function () use (&$rectangle0, &$a, &$b) {
return sprintf("%s: %s x %s", $rectangle0["get_name"](), $a, $b);
},
];
return $rectangle0;
};
$rectangle1 = $rectangle(2, 3);
$rectangle1["get_perimeter"](); // result: 10
$rectangle1["get_area"](); // result: 6
$rectangle2 = $rectangle(4, 4);
$rectangle2["get_perimeter"](); // result: 16
$rectangle2["set_a"](5);
$rectangle2["get_perimeter"](); // result: 18
More general scheme of Revealing Module pattern with explanation of certain parts in comments looks as follows:
// 1) $obj is function acting as CONSTRUCTOR
// - passed parameters $param1, $param2 can be later accessed by any private or public properties/functions defined in constructor
$obj = function ($param1, $param2) {
// 2) PRIVATE properties/functions are implemented as local variables
$private_prop1 = 10;
$private_func1 = function () use (&$obj0, &$private_prop1, &$param1, &$private_func1) {
// private function $private_func1 can access other private properties/functions
$private_prop1++;
// private function $private_func1 can access public properties/functions via $obj0 reference (see below)
$obj0["public_prop1"] += $param1;
// function $private_func1 is also able to call itself recursively via $private_func1 reference
if (false) $private_func1();
};
// 3) PUBLIC properties/functions are implemented as values in $obj0 array
// - $obj0 itself represents newly created object instance and is returned from constructor function
$obj0 = [
"public_prop1" => 20,
"public_func1" => function () use (&$obj0, &$private_func1, &$param2) {
// public function public_func1 can access other public properties/functions via $obj0 reference
$obj0["public_prop1"] += $param2;
// public function public_func1 can access private properties/functions
$private_func1();
},
];
return $obj0;
};
// 5) INSTANCE $obj1 will be created by calling constructor function $obj
$obj1 = $obj(10, 20);
$obj1["public_func1"]();
This snippet comprises roughly everything you need to know about Revealing Module.
When using Revealing Module in PHP you should specifically be careful about use (...)
construct, which needs to declare all variables from the outer scope that your function needs to use. Especially ensure you declare variables by reference (use (&$private_prop1)
) instead of by value (use ($private_prop1)
), otherwise the variable value in the inner scope will not match future changes of the variable in outer scope and it also won't be possible to edit the variable from the inner scope.
Finally, it would be very interesting to look at pros and cons of Revealing Module approach in comparison with traditional PHP OOP.
- Revealing Module fits well into procedural codebase, in case you don't want to mix it with OOP. It also doesn't require any knowledge of OOP programming.
- It is suitable for simple ad-hoc objects you create inside of other existing module or script. In such cases you don't necessarily want to create full OOP class in separate file. Revealing Module constructors can be created in the middle of existing code, even inside loops (constructor function redefinition doesn't imply any error).
- Object instance is implemented as an array and is quite flexible. You can add/delete public properties and functions, sort them and build nested structures from them (e.g.
$obj1["functions_group1"]["function1"]();
: member functionfunction1
has been placed intofunctions_group1
, which is just a nested array of functions). -
Constructor function is standard variable (
Closure
object) and can be manipulated easily.- You can store it in another variable, return it from a function or pass it as a parameter:
$foo = fn($rectangle) => $rectangle(4,5); $foo($rectangle);
- Standard OOP class on the other hand is not a variable and you can pass it as a parameter only using its name obtained from
::class
constant:
$foo = fn($className) => new $className(4,5); $foo(Rectangle::class);
This list is very extensive, that's why you should use Revealing Module only sparingly.
-
Inheritance, cloning and serialization of Revealing Module objects are not possible. (Especially lack of inheritance is quite painful, because it is strong mechanism of code reuse.) Determining object type using
instanceof
orget_class()
is not possible. - Type hinting of private or public object properties and functions is not possible.
- IDE support is poor. You probably will not get any autocomplete for public object properties and functions, because they are implemented as simple array values. You cannot document member properties and functions with PHPDoc.
-
Autoloading is not possible. If your constructor function is located in a separate file, you will need to
include
it manually. - Code structure consisting of private and public members is not as optically clear as with classic OOP. Necessity to put so many variables into
use (...)
construct is annoying and causes code bloat. - Object instances are simple arrays and, as such, are passed by value by default. It is undesirable, because we are used to objects being passed by reference.
- It is not possible to implement static object properties and functions. You always need to create instance first and call instance properties and functions.
I hope this article has inspired you to use functional programming in PHP more frequently, or even try out Revealing Module pattern in PHP or any other functional-ready language. For common work on PHP backend, you should probably stick to traditional OOP approach.
27