index.php
10.8 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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
<?php
namespace mindplay\demo;
use Composer\Autoload\ClassLoader;
use mindplay\annotations\AnnotationCache;
use mindplay\annotations\Annotations;
use mindplay\demo\annotations\Package;
## Configure a simple auto-loader
$vendor_path = dirname(__DIR__) . '/vendor';
if (!is_dir($vendor_path)) {
echo 'Install dependencies first' . PHP_EOL;
exit(1);
}
require_once($vendor_path . '/autoload.php');
$auto_loader = new ClassLoader();
$auto_loader->addPsr4("mindplay\\demo\\", __DIR__);
$auto_loader->register();
## Configure the cache-path. The static `Annotations` class will configure any public
## properties of `AnnotationManager` when it creates it. The `AnnotationManager::$cachePath`
## property is a path to a writable folder, where the `AnnotationManager` caches parsed
## annotations from individual source code files.
Annotations::$config['cache'] = new AnnotationCache(__DIR__ . '/runtime');
## Register demo annotations.
Package::register(Annotations::getManager());
## For this example, we're going to generate a simple form that allows us to edit a `Person`
## object. We'll define a few public properties and annotate them with some useful metadata,
## which will enable us to make decisions (at run-time) about how to display each field,
## how to parse the values posted back from the form, and how to validate the input.
##
## Note the use of standard PHP-DOC annotations, such as `@var string` - this metadata is
## traditionally useful both as documentation to developers, and as hints for an IDE. In
## this example, we're going to use that same information as advice to our components, at
## run-time, to help them establish defaults and make sensible decisions about how to
## handle the value of each property.
class Person
{
/**
* @var string
* @required
* @length(50)
* @text('label' => 'Full Name')
*/
public $name;
/**
* @var string
* @length(50)
* @text('label' => 'Street Address')
*/
public $address;
/**
* @var int
* @range(0, 100)
*/
public $age;
}
## To build a simple form abstraction that can manage the state of an object being edited,
## we start with a simple, abstract base class for input widgets.
abstract class Widget
{
protected $object;
protected $property;
public $value;
## Each widget will maintain a list of error messages.
public $errors = array();
## A widget needs to know which property of what object is being edited.
public function __construct($object, $property)
{
$this->object = $object;
$this->property = $property;
$this->value = $object->$property;
}
## Widget classes will use this method to add an error-message.
public function addError($message)
{
$this->errors[] = $message;
}
## This helper function provides a shortcut to get a named property from a
## particular type of annotation - if no annotation is found, the `$default`
## value is returned instead.
protected function getMetadata($type, $name, $default = null)
{
$a = Annotations::ofProperty($this->object, $this->property, $type);
if (!count($a)) {
return $default;
}
return $a[0]->$name;
}
## Each type of widget will need to implement this interface, which takes a raw
## POST value from the form, and attempts to bind it to the object's property.
abstract public function update($input);
## After a widget successfully updates a property, we may need to perform additional
## validation - this method will perform some basic validations, and if errors are
## found, it will add them to the `$errors` collection.
public function validate()
{
if (empty($this->value)) {
if ($this->isRequired()) {
$this->addError("Please complete this field");
} else {
return;
}
}
if (is_string($this->value)) {
$min = $this->getMetadata('@length', 'min');
$max = $this->getMetadata('@length', 'max');
if ($min !== null && strlen($this->value) < $min) {
$this->addError("Minimum length is {$min} characters");
} else {
if ($max !== null && strlen($this->value) > $max) {
$this->addError("Maximum length is {$max} characters");
}
}
}
if (is_int($this->value)) {
$min = $this->getMetadata('@range', 'min');
$max = $this->getMetadata('@range', 'max');
if (($min !== null && $this->value < $min) || ($max !== null && $this->value > $max)) {
$this->addError("Please enter a value in the range {$min} to {$max}");
}
}
}
## Each type of widget will need to implement this interface, which renders an
## HTML input representing the widget's current value.
abstract public function display();
## This helper function returns a descriptive label for the input.
public function getLabel()
{
return $this->getMetadata('@text', 'label', ucfirst($this->property));
}
## Finally, this little helper function will tell us if the field is required -
## if a property is annotated with `@required`, the field must be filled in.
public function isRequired()
{
return count(Annotations::ofProperty($this->object, $this->property, '@required')) > 0;
}
}
## The first and most basic kind of widget, is this simple string widget.
class StringWidget extends Widget
{
## On update, take into account the min/max string length, and provide error
## messages if the constraints are violated.
public function update($input)
{
$this->value = $input;
$this->validate();
}
## On display, render out a simple `<input type="text"/>` field, taking into account
## the maximum string-length.
public function display()
{
$length = $this->getMetadata('@length', 'max', 255);
echo '<input type="text" name="' . get_class($this->object) . '[' . $this->property . ']"'
. ' maxlength="' . $length . '" value="' . htmlspecialchars($this->value) . '"/>';
}
}
## For the age input, we'll need a specialized `StringWidget` that also checks the input type.
class IntWidget extends StringWidget
{
## On update, take into account the min/max numerical range, and provide error
## messages if the constraints are violated.
public function update($input)
{
if (strval(intval($input)) === $input) {
$this->value = intval($input);
$this->validate();
} else {
$this->value = $input;
if (!empty($input)) {
$this->addError("Please enter a whole number value");
}
}
}
}
## Next, we can build a simple form abstraction - this will hold and object and manage
## the widgets required to edit the object.
class Form
{
private $object;
/**
* Widget list.
*
* @var Widget[]
*/
private $widgets = array();
## The constructor just needs to know which object we're editing.
##
## Using reflection, we enumerate the properties of the object's type, and using the
## `@var` annotation, we decide which type of widget we're going to use.
public function __construct($object)
{
$this->object = $object;
$class = new \ReflectionClass($this->object);
foreach ($class->getProperties() as $property) {
$type = $this->getMetadata($property->name, '@var', 'type', 'string');
$wtype = 'mindplay\\demo\\' . ucfirst($type) . 'Widget';
$this->widgets[$property->name] = new $wtype($this->object, $property->name);
}
}
## This helper-method is similar to the one we defined for the widget base
## class, but fetches annotations for the specified property.
private function getMetadata($property, $type, $name, $default = null)
{
$a = Annotations::ofProperty(get_class($this->object), $property, $type);
if (!count($a)) {
return $default;
}
return $a[0]->$name;
}
## When you post information back to the form, we'll need to update it's state,
## validate each of the fields, and return a value indicating whether the form
## update was successful.
public function update($post)
{
$data = $post[get_class($this->object)];
foreach ($this->widgets as $property => $widget) {
if (array_key_exists($property, $data)) {
$this->widgets[$property]->update($data[$property]);
}
}
$valid = true;
foreach ($this->widgets as $widget) {
$valid = $valid && (count($widget->errors) === 0);
}
if ($valid) {
foreach ($this->widgets as $property => $widget) {
$this->object->$property = $widget->value;
}
}
return $valid;
}
## Finally, this method renders out the form, and each of the widgets inside, with
## a `<label>` tag surrounding each input.
public function display()
{
foreach ($this->widgets as $widget) {
$star = $widget->isRequired() ? ' <span style="color:red">*</span>' : '';
echo '<label>' . htmlspecialchars($widget->getLabel()) . $star . '<br/>';
$widget->display();
echo '</label><br/>';
if (count($widget->errors)) {
echo '<ul>';
foreach ($widget->errors as $error) {
echo '<li>' . htmlspecialchars($error) . '</li>';
}
echo '</ul>';
}
}
}
}
## Now let's put the whole thing to work...
##
## We'll create a `Person` object, create a `Form` for the object, and render it!
##
## Try leaving the name field empty, or try to tell the form you're 120 years old -
## it won't pass validation.
##
## You can see the state of the object being displayed below the form - as you can
## see, unless all updates and validations succeed, the state of your object is
## left untouched.
echo <<<HTML
<html>
<head>
<title>Metaprogramming With Annotations!</title>
</head>
<body>
<h1>Edit a Person!</h1>
<h4>Declarative Metaprogramming in action!</h4>
<form method="post">
HTML;
$person = new Person;
$form = new Form($person);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($form->update($_POST)) {
echo '<h2 style="color:green">Person Accepted!</h2>';
} else {
echo '<h2 style="color:red">Oops! Try again.</h2>';
}
}
$form->display();
echo <<<HTML
<br/>
<input type="submit" value="Go!"/>
</form>
HTML;
echo "<pre>\n\nHere's what your Person instance currently looks like:\n\n";
var_dump($person);
echo '</pre>';
echo <<<HTML
</body>
</html>
HTML;