审查视图

simplewind/vendor/mindplay/annotations/demo/index.php 10.8 KB
xiaohu authored
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;