123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372 |
- <?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;
|