-
-
Notifications
You must be signed in to change notification settings - Fork 277
Expand file tree
/
Copy pathsmartobject.texy
More file actions
240 lines (180 loc) Β· 8.64 KB
/
smartobject.texy
File metadata and controls
240 lines (180 loc) Β· 8.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
SmartObject
***********
.[perex]
SmartObject enhanced PHP object behavior for many years. Since PHP 8.4, all of its features have become native parts of PHP itself, thus completing its historic mission as a pioneer of the modern object-oriented approach in PHP.
Installation:
```shell
composer require nette/utils
```
SmartObject was introduced in 2007 as a revolutionary solution to the shortcomings of PHP's object model at the time. In an era when PHP faced numerous issues with object-oriented design, it brought significant improvements and simplified developers' workflows. It became a legendary component of the Nette Framework. SmartObject offered functionality that PHP only acquired many years later β from access control for object properties to sophisticated syntactic sugar. With the release of PHP 8.4, it fulfilled its historical mission, as all its features became a native part of the language. It was ahead of PHP's development by an impressive 17 years.
Technically, SmartObject went through an interesting evolution. Initially, it was implemented as the `Nette\Object` class, from which other classes inherited the needed functionality. A significant change came with PHP 5.4, which introduced trait support. This enabled transformation into the `Nette\SmartObject` trait, bringing greater flexibility - developers could use the functionality even in classes that already inherited from another class. While the original `Nette\Object` class ceased to exist with PHP 7.2 (which prohibited naming classes with the word `Object`), the `Nette\SmartObject` trait lives on.
Let's explore the features that `Nette\Object` and later `Nette\SmartObject` offered. Each of these functions represented a significant step forward in PHP object-oriented programming at the time.
Consistent Error States
-----------------------
One of the most pressing issues of early PHP was inconsistent behavior when working with objects. `Nette\Object` brought order and predictability to this chaos. Let's look at how PHP originally behaved:
```php
echo $obj->undeclared; // E_NOTICE, later E_WARNING
$obj->undeclared = 1; // passes silently without warning
$obj->unknownMethod(); // Fatal error (uncatchable by try/catch)
```
A fatal error would terminate the application without any possibility to react. Silently writing to non-existent members without warning could lead to serious errors that were difficult to detect. `Nette\Object` caught all these cases and threw a `MemberAccessException`, allowing programmers to react to and handle these errors:
```php
echo $obj->undeclared; // throws Nette\MemberAccessException
$obj->undeclared = 1; // throws Nette\MemberAccessException
$obj->unknownMethod(); // throws Nette\MemberAccessException
```
Since PHP 7.0, the language no longer causes uncatchable fatal errors, and since PHP 8.2, access to undeclared members is considered an error.
"Did you mean?" Helper
----------------------
`Nette\Object` came with a very convenient feature: intelligent suggestions for typos. When a developer made a mistake in a method or variable name, it not only reported the error but also offered help by suggesting the correct name. This iconic message, known as "did you mean?", saved programmers hours of hunting for typos:
```php
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// throws Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"
```
While current PHP doesn't have any form of "did you mean?", this suffix can be added to errors by [Tracy|tracy:]. And it can even [auto-fix |tracy:open-files-in-ide#Demos] such errors.
Properties with Controlled Access
---------------------------------
A significant innovation that SmartObject brought to PHP was properties with controlled access. This concept, common in languages like C# or Python, allowed developers to elegantly control access to object data and ensure their consistency. Properties are a powerful tool of object-oriented programming. They function like variables but are actually represented by methods (getters and setters). This allows input validation or value generation at the time of reading.
To use properties, you had to:
- Add the annotation `@property <type> $xyz` to the class
- Create a getter named `getXyz()` or `isXyz()`, a setter named `setXyz()`
- Ensure the getter and setter were *public* or *protected*. They were optional - thus could exist as *read-only* or *write-only* properties
Let's look at a practical example using the `Circle` class, where we'll use properties to ensure that the radius is always non-negative. We'll replace `public $radius` with a property:
```php
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // not public!
// getter for $radius property
protected function getRadius(): float
{
return $this->radius;
}
// setter for $radius property
protected function setRadius(float $radius): void
{
// sanitize the value before saving
$this->radius = max(0.0, $radius);
}
// getter for $visible property
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // actually calls setRadius(10)
echo $circle->radius; // calls getRadius()
echo $circle->visible; // calls isVisible()
```
Since PHP 8.4, the same functionality can be achieved using property hooks, which offer a much more elegant and concise syntax:
```php
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
```
Extension Methods
-----------------
`Nette\Object` brought another interesting concept to PHP inspired by modern programming languages - extension methods. This feature, borrowed from C#, allowed developers to elegantly extend existing classes with new methods without modifying them or inheriting from them. For instance, you could add an `addDateTime()` method to a form that adds a custom DateTimePicker:
```php
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
```
Extension methods proved impractical because their names were not suggested by code editors, which instead reported that the method did not exist. Therefore, their support was discontinued. Today, it's more common to use composition or inheritance to extend class functionality.
Getting Class Name
------------------
SmartObject offered a simple method for getting the class name:
```php
$class = $obj->getClass(); // using Nette\Object
$class = $obj::class; // since PHP 8.0
```
Reflection and Annotation Access
--------------------------------
`Nette\Object` provided access to reflection and annotations through the methods `getReflection()` and `getAnnotation()`. This approach significantly simplified working with class meta-information:
```php
/**
* @author John Doe
*/
class Foo extends Nette\Object
{
}
$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // returns 'John Doe'
```
Since PHP 8.0, it's possible to access meta-information through attributes, which offer even more possibilities and better type checking:
```php
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
```
Method Getters
--------------
`Nette\Object` offered an elegant way to pass methods as if they were variables:
```php
class Foo extends Nette\Object
{
public function adder($a, $b)
{
return $a + $b;
}
}
$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5
```
Since PHP 8.1, you can use the "first-class callable syntax":https://www.php.net/manual/en/functions.first_class_callable_syntax.php, which takes this concept even further:
```php
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
```
Events
------
SmartObject offers simplified syntax for working with [events |nette:glossary#Events]. Events allow objects to inform other parts of the application about changes in their state:
```php
class Circle extends Nette\Object
{
public array $onChange = [];
public function setRadius(float $radius): void
{
$this->onChange($this, $radius);
$this->radius = $radius;
}
}
```
The code `$this->onChange($this, $radius)` is equivalent to the following loop:
```php
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
```
For clarity, we recommend avoiding the magic `$this->onChange()` method. A practical replacement is the [Nette\Utils\Arrays::invoke |arrays#invoke] function:
```php
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);
```