├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── config ├── autoload │ ├── .gitignore │ ├── dependencies.global.php │ ├── local.php.dist │ ├── middleware-pipeline.global.php │ ├── routes.global.php │ ├── templates.global.php │ └── zend-expressive.global.php ├── config.php └── container.php ├── data └── .gitignore ├── doc ├── .gitignore ├── book │ ├── images │ │ ├── install-zend-db.png │ │ ├── install-zend-hydrator.png │ │ ├── install-zend-inputfilter-form.png │ │ ├── installer.png │ │ ├── screen-after-installation.png │ │ ├── screen-album-create-form.png │ │ ├── screen-album-delete.png │ │ ├── screen-album-list-links.png │ │ ├── screen-album-list-no-data.png │ │ ├── screen-album-list-with-data.png │ │ └── screen-album-update.png │ ├── part1.md │ ├── part2.md │ ├── part3.md │ ├── part4.md │ ├── part5.md │ └── part6.md └── bookdown.json ├── phpcs.xml ├── phpunit.xml.dist ├── public ├── .htaccess ├── favicon.ico ├── index.php └── zf-logo.png ├── src └── App │ └── Action │ ├── HomePageAction.php │ ├── HomePageFactory.php │ └── PingAction.php ├── templates ├── .gitkeep ├── app │ └── home-page.phtml ├── error │ ├── 404.phtml │ └── error.phtml └── layout │ └── default.phtml └── test └── AppTest └── Action ├── HomePageActionTest.php ├── HomePageFactoryTest.php └── PingActionTest.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: clover.xml 2 | json_path: coveralls-upload.json 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.phar 3 | 4 | clover.xml 5 | coveralls-upload.json 6 | phpunit.xml 7 | vendor/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | cache: 6 | directories: 7 | - $HOME/.composer/cache 8 | - vendor 9 | 10 | env: 11 | global: 12 | - COMPOSER_ARGS="--no-interaction --ignore-platform-reqs --no-scripts" 13 | 14 | notifications: 15 | irc: "irc.freenode.org#zftalk.dev" 16 | email: false 17 | 18 | matrix: 19 | fast_finish: true 20 | include: 21 | - php: 5.5 22 | env: 23 | - DEPS=lowest 24 | - php: 5.5 25 | env: 26 | - DEPS=latest 27 | - php: 5.6 28 | env: 29 | - DEPS=lowest 30 | - php: 5.6 31 | env: 32 | - DEPS=latest 33 | - TEST_COVERAGE=true 34 | - php: 7 35 | env: 36 | - DEPS=lowest 37 | - php: 7 38 | env: 39 | - DEPS=latest 40 | - CHECK_CS=true 41 | - php: hhvm 42 | allow_failures: 43 | - php: hhvm 44 | 45 | before_install: 46 | - if [[ $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi 47 | - travis_retry composer self-update 48 | 49 | install: 50 | - if [[ $DEPS == 'latest' ]]; then travis_retry composer update $COMPOSER_ARGS ; fi 51 | - if [[ $DEPS == 'lowest' ]]; then travis_retry composer update --prefer-lowest --prefer-stable $COMPOSER_ARGS ; fi 52 | - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer require --dev $COMPOSER_ARGS satooshi/php-coveralls:^1.0 ; fi 53 | - travis_retry composer install $COMPOSER_ARGS 54 | - composer show --installed 55 | 56 | script: 57 | - if [[ $TEST_COVERAGE == 'true' ]]; then composer test-coverage ; fi 58 | - if [[ $TEST_COVERAGE != 'true' ]]; then composer test ; fi 59 | - if [[ $CHECK_CS == 'true' ]]; then composer cs-check ; fi 60 | 61 | after_script: 62 | - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer upload-coverage ; fi 63 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file, in reverse chronological order by release. 4 | 5 | ## 1.0.3 - 2016-09-01 6 | 7 | ### Added 8 | 9 | - [#93](https://github.com/zendframework/zend-expressive-skeleton/pull/93) adds 10 | support for Pimple "extensions" (`$pimple->extend()`) via the `dependencies` 11 | sub-key `extensions`, as follows: 12 | 13 | ```php 14 | return [ 15 | 'dependencies' => [ 16 | SomeClass::class => ExtendingFactory::class, 17 | ], 18 | ]; 19 | ``` 20 | 21 | - [#93](https://github.com/zendframework/zend-expressive-skeleton/pull/93) adds 22 | support to the Pimple container script to allow wrapping `delegators` 23 | (delegator factories from zend-servicemanager) as anonymous Pimple extensions. 24 | 25 | ### Deprecated 26 | 27 | - Nothing. 28 | 29 | ### Removed 30 | 31 | - [#102](https://github.com/zendframework/zend-expressive-skeleton/pull/102) 32 | removes the development dependendy on ocramius/proxy-manager, as it is not 33 | required. 34 | 35 | ### Fixed 36 | 37 | - [#91](https://github.com/zendframework/zend-expressive-skeleton/pull/91) fixes 38 | the Pimple factory caching to work correctly with invokable classes used as 39 | factories. 40 | - [#95](https://github.com/zendframework/zend-expressive-skeleton/pull/95) fixes 41 | the prompt for a minimal install to ensure that only `n` and `y` (or uppercase 42 | versions of each) are valid answers, looping until a valid answer is provided. 43 | - [#101](https://github.com/zendframework/zend-expressive-skeleton/pull/101) 44 | removes filp/whoops from the `composer.json` prior to prompting the user for 45 | packages to install, ensuring it does not remain if a user selects a minimal 46 | install or to not use whoops for development. 47 | - [#109](https://github.com/zendframework/zend-expressive-skeleton/pull/109) 48 | adds comprehensive, granular tests covering all functionality of the 49 | installer, raising coverage from 40% to 100%. 50 | 51 | ## 1.0.2 - 2016-04-21 52 | 53 | ### Added 54 | 55 | - Nothing. 56 | 57 | ### Deprecated 58 | 59 | - Nothing. 60 | 61 | ### Removed 62 | 63 | - Nothing. 64 | 65 | ### Fixed 66 | 67 | - [#85](https://github.com/zendframework/zend-expressive-skeleton/pull/85) 68 | updates the Aura.Di dependency to stable 3.X versions. 69 | - [#88](https://github.com/zendframework/zend-expressive-skeleton/pull/88) 70 | modifies the installer to remove `composer.lock` from the `.gitignore` file 71 | during initial installation. 72 | - [#89](https://github.com/zendframework/zend-expressive-skeleton/pull/89) 73 | updates the zend-stdlib dependency to allow usage of its v3 series. 74 | 75 | ## 1.0.1 - 2016-03-17 76 | 77 | ### Added 78 | 79 | - Nothing. 80 | 81 | ### Deprecated 82 | 83 | - Nothing. 84 | 85 | ### Removed 86 | 87 | - Nothing. 88 | 89 | ### Fixed 90 | 91 | - [#53](https://github.com/zendframework/zend-expressive-skeleton/pull/53) 92 | updates the default Pimple container script such that it now caches factory 93 | instances for re-use. 94 | - [#72](https://github.com/zendframework/zend-expressive-skeleton/pull/72) 95 | updates the `composer.json` to remove the possibility of installing an 96 | Expressive RC version, updates zend-servicemanager to allow using 3.0 97 | versions, and updates whoops to allow either 1.1 or 2.0 versions. 98 | - [#80](https://github.com/zendframework/zend-expressive-skeleton/pull/80) 99 | updates the default ProxyManager constraints to also allow v2 versions. 100 | - [#81](https://github.com/zendframework/zend-expressive-skeleton/pull/81) 101 | fixes an issue in the installer whereby specified constraints were not being 102 | passed to Composer prior to dependency resolution/installation, resulting in 103 | stale dependencies. 104 | - [#78](https://github.com/zendframework/zend-expressive-skeleton/pull/78) 105 | updates the shipped default error templates to remove error/exception display. 106 | Users who really need this functionality can write their own templates; the 107 | project aims to deliver a "safe by default" setting. 108 | 109 | ## 1.0.0 - 2016-01-28 110 | 111 | First stable release. 112 | 113 | ### Added 114 | 115 | - Nothing. 116 | 117 | ### Deprecated 118 | 119 | - Nothing. 120 | 121 | ### Removed 122 | 123 | - Nothing. 124 | 125 | ### Fixed 126 | 127 | - [#69](https://github.com/zendframework/zend-expressive-skeleton/pull/69) 128 | updates the links in templates to point to the new documentation site on 129 | https://zendframework.github.io/zend-expressive/ instead of rtfd.org. 130 | 131 | ## 1.0.0rc8 - 2016-01-21 132 | 133 | Eighth release candidate. 134 | 135 | ### Added 136 | 137 | - Nothing. 138 | 139 | ### Deprecated 140 | 141 | - Nothing. 142 | 143 | ### Removed 144 | 145 | - Nothing. 146 | 147 | ### Fixed 148 | 149 | - [#66](https://github.com/zendframework/zend-expressive-skeleton/pull/66) 150 | adds the `'error' => true,` declaration to the `'error'` pipeline middleware 151 | specification. 152 | - [#67](https://github.com/zendframework/zend-expressive-skeleton/pull/67) 153 | updates the `filp/whoops` dependency for installer development to `^1.1 || ^2.0`; 154 | the two are compatible for our use cases, but we should prefer the latest 155 | that can be installed. As 2.0 requires PHP 5.5.9, but our minimum PHP version 156 | is 5.5.0, we must specify both. 157 | 158 | ## 1.0.0rc7 - 2016-01-19 159 | 160 | Seventh release candidate. 161 | 162 | ### Added 163 | 164 | - Nothing. 165 | 166 | ### Deprecated 167 | 168 | - Nothing. 169 | 170 | ### Removed 171 | 172 | - Nothing. 173 | 174 | ### Fixed 175 | 176 | - [#64](https://github.com/zendframework/zend-expressive-skeleton/pull/64) 177 | fixes the installer script to correctly rewrite the `require-dev` section 178 | and ensure only the development dependencies selected, as well as base 179 | requirements such as PHPUnit and PHP_CodeSniffer, are installed. As such, 180 | the `--no-dev` flag is no longer required, and development dependencies 181 | such as whoops are properly installed. 182 | 183 | ## 1.0.0rc6 - 2016-01-19 184 | 185 | Sixth release candidate. 186 | 187 | ### Added 188 | 189 | - Nothing. 190 | 191 | ### Deprecated 192 | 193 | - Nothing. 194 | 195 | ### Removed 196 | 197 | - Nothing. 198 | 199 | ### Fixed 200 | 201 | - [#56](https://github.com/zendframework/zend-expressive-skeleton/pull/56) 202 | updates the `composer serve` command to include the `public/index.php` script 203 | as an argument. This ensures that asset paths that the application could 204 | intercept and serve will be passed to the application (previously, the 205 | built-in server would treat these as 404s, and never pass them to the 206 | application). 207 | - [#57](https://github.com/zendframework/zend-expressive-skeleton/pull/57) 208 | updates the Apache configuration rules defined in `public/.htaccess` to omit 209 | several that could prevent the application from intercepting requests for 210 | assets. 211 | - [#52](https://github.com/zendframework/zend-expressive-skeleton/pull/52) 212 | fixes the switch statement in the `HomePageAction` class to ensure the 213 | template name and documentation link are accurately found. 214 | - [#59](https://github.com/zendframework/zend-expressive-skeleton/pull/59) 215 | updates the `config/container.php` implementation for zend-servicemanager such 216 | that it can work with either v2 or v3 of that library. 217 | - [#60](https://github.com/zendframework/zend-expressive-skeleton/pull/60) 218 | updates the zend-expressive-helpers dependency to `^2.0`, and updates the 219 | `config/autoload/middleware-pipeline.global.php` to follow the changes in 220 | middleware configuration introduced in [zend-expressive #270](https://github.com/zendframework/zend-expressive/pull/270). 221 | The change introduces convention-based keys for "always" (execute before 222 | routing), "routing" (routing, listeners that act on the route result, and 223 | dispatching), and "error", with reasonable priorities to ensure execution 224 | order. 225 | - [#60](https://github.com/zendframework/zend-expressive-skeleton/pull/60) 226 | fixes the documentation for `composer create-project` to include the 227 | `--no-dev` flag; this is done as composer currently installs the development 228 | dependencies listed before the installer script rewrites the `composer.json` 229 | file. Running `composer update` or `composer install` within the project 230 | directory after the initial installation will install the development 231 | dependencies. 232 | 233 | ## 1.0.0rc5 - 2015-12-22 234 | 235 | Fifth release candidate. 236 | 237 | ### Added 238 | 239 | - Nothing. 240 | 241 | ### Deprecated 242 | 243 | - Nothing. 244 | 245 | ### Removed 246 | 247 | - Nothing. 248 | 249 | ### Fixed 250 | 251 | - [#42](https://github.com/zendframework/zend-expressive-skeleton/pull/42) 252 | fixes some grammatical issues in the questions presented by the installer. 253 | - [#45](https://github.com/zendframework/zend-expressive-skeleton/pull/45) 254 | fixes how JS and CSS assets are added to zend-view templates. 255 | - [#48](https://github.com/zendframework/zend-expressive-skeleton/pull/48) 256 | adds unit tests for the `OptionalPackages` class (which provides the Composer 257 | installer scripts). 258 | - [#49](https://github.com/zendframework/zend-expressive-skeleton/pull/49) 259 | updates the Pimple support to Pimple v3, ensuring Pimple users are using the 260 | latest stable release. 261 | 262 | ## 1.0.0rc4 - 2015-12-09 263 | 264 | Fourth release candidate. 265 | 266 | ### Added 267 | 268 | - [#34](https://github.com/zendframework/zend-expressive-skeleton/pull/34) 269 | updates the zend-view configuration to register a factory for 270 | `Zend\View\HelperPluginManager`, as well as a `view_helpers` sub-key for 271 | registering custom view helpers. 272 | - [#37](https://github.com/zendframework/zend-expressive-skeleton/pull/37) 273 | creates the subdirectories `src/App/` and `test/AppTest/`, moving the 274 | subdirectories of each under those, and updating the `composer.json` 275 | autoloading directives accordingly. This change will allow new projects to 276 | implement a "modular" structure if desired, with a subdirectory per namespace. 277 | - [#41](https://github.com/zendframework/zend-expressive-skeleton/pull/41) adds 278 | the composer script "serve", which fires up the built-in PHP webserver on port 279 | 8080; invoke using `composer serve`. 280 | 281 | ### Deprecated 282 | 283 | - Nothing. 284 | 285 | ### Removed 286 | 287 | - Nothing. 288 | 289 | ### Fixed 290 | 291 | - [#23](https://github.com/zendframework/zend-expressive-skeleton/pull/23) 292 | updates the comment for the glob statements to ensure all 4 (not just 2!) 293 | possible matches are detailed. 294 | - [#24](https://github.com/zendframework/zend-expressive-skeleton/pull/24) 295 | updates the `config/config.php` file to store cached configuration as a plain 296 | PHP file, so that it can simply `include()`; this will be faster than using 297 | JSON-serialized structures. 298 | - [#30](https://github.com/zendframework/zend-expressive-skeleton/pull/30) 299 | updates the Twig configuration to follow the changes made for 300 | [zendframework/zend-expressive-twigrenderer 0.3.0](https://github.com/zendframework/zend-expressive-twigrenderer/releases/tag/0.3.0). 301 | The old configuration format will still work, though users *should* update 302 | their configuration to the new format. The change in this patch only affects 303 | new installs. 304 | - [#33](https://github.com/zendframework/zend-expressive-skeleton/pull/33) 305 | updates to zendframework/zend-expressive-helpers `^1.2`. 306 | - [#33](https://github.com/zendframework/zend-expressive-skeleton/pull/33) adds 307 | configuration for auto-registering the new `Zend\Expressive\Helper\UrlHelperMiddleware` 308 | as pipeline middleware; this fixes an issue when using the zend-view renderer 309 | with the `url()` helper whereby the `UrlHelper` was being registered as a 310 | route result observer too late to receive the `RouteResult`. 311 | - [#40](https://github.com/zendframework/zend-expressive-skeleton/pull/40) 312 | renames the namespace for the installer to `ExpressiveInstaller`. 313 | 314 | ## 1.0.0rc3 - 2015-12-07 315 | 316 | Third release candidate. 317 | 318 | ### Added 319 | 320 | - [#20](https://github.com/zendframework/zend-expressive-skeleton/pull/20) adds 321 | the ability to specify a "minimal" install; when selected, the installer will 322 | install modified configuration, omit some files, and remove the default 323 | middleware and public assets. 324 | - [#27](https://github.com/zendframework/zend-expressive-skeleton/pull/27) adds 325 | [zendframework/zend-expressive-helpers](https://github.com/zendframework/zend-expressive-helpers) 326 | as a dependency, and integrates the helpers into the configuration. 327 | 328 | ### Deprecated 329 | 330 | - Nothing. 331 | 332 | ### Removed 333 | 334 | - Nothing. 335 | 336 | ### Fixed 337 | 338 | - [#13](https://github.com/zendframework/zend-expressive-skeleton/pull/13) 339 | updates the installer to also remove the dependency on composer/composer 340 | on completion. 341 | - [#11](https://github.com/zendframework/zend-expressive-skeleton/pull/11) 342 | moves the route middleware service definitions into the routes configuration 343 | files. 344 | - [#21](https://github.com/zendframework/zend-expressive-skeleton/pull/21) 345 | updates `require` statements in generated configuration files to use the 346 | `__DIR__` constant to ensure files are located relative to the origin file. 347 | - [#25](https://github.com/zendframework/zend-expressive-skeleton/pull/25) and 348 | [#29](https://github.com/zendframework/zend-expressive-skeleton/pull/29) 349 | update minimum versions for each router and template implementation (final 350 | versions for RC3 are all at `^1.0`). 351 | - [#29](https://github.com/zendframework/zend-expressive-skeleton/pull/29) sets 352 | the zend-expressive required version to `~1.0.0@rc || ^1.0`, to ensure a 353 | stable version is always installed. 354 | 355 | ## 1.0.0rc2 - 2015-10-20 356 | 357 | Second release candidate. 358 | 359 | ### Added 360 | 361 | - Nothing. 362 | 363 | ### Deprecated 364 | 365 | - Nothing. 366 | 367 | ### Removed 368 | 369 | - Nothing. 370 | 371 | ### Fixed 372 | 373 | - Updated expressive to RC2. 374 | - Updated subcomponent versions in installer to `^0.2` 375 | 376 | ## 1.0.0rc1 - 2015-10-19 377 | 378 | First release candidate. 379 | 380 | ### Added 381 | 382 | - Nothing. 383 | 384 | ### Deprecated 385 | 386 | - Nothing. 387 | 388 | ### Removed 389 | 390 | - Nothing. 391 | 392 | ### Fixed 393 | 394 | - Nothing. 395 | 396 | ## 0.5.3 - 2015-10-16 397 | 398 | ### Added 399 | 400 | - [#8](https://github.com/zendframework/zend-expressive-skeleton/pull/8) adds a 401 | routine to the installer that recursively removes the `src/Composer/` 402 | directory of the skeleton, ensuring you have a clean start when creating a 403 | project. 404 | 405 | ### Deprecated 406 | 407 | - Nothing. 408 | 409 | ### Removed 410 | 411 | - Nothing. 412 | 413 | ### Fixed 414 | 415 | - Nothing. 416 | 417 | ## 0.5.2 - 2015-10-13 418 | 419 | ### Added 420 | 421 | - [#7](https://github.com/zendframework/zend-expressive-skeleton/pull/7) adds a 422 | dependency on zend-stdlib for the purposes of globbing and merging 423 | configuration. 424 | 425 | ### Deprecated 426 | 427 | - Nothing. 428 | 429 | ### Removed 430 | 431 | - Nothing. 432 | 433 | ### Fixed 434 | 435 | - Nothing. 436 | 437 | ## 0.5.1 - 2015-10-11 438 | 439 | ### Added 440 | 441 | - Nothing. 442 | 443 | ### Deprecated 444 | 445 | - Nothing. 446 | 447 | ### Removed 448 | 449 | - Nothing. 450 | 451 | ### Fixed 452 | 453 | - [#6](https://github.com/zendframework/zend-expressive-skeleton/pull/6) updates 454 | the zendframework/zend-view package configuration to remove the dependency on 455 | zendframework/zend-i18n, as it is now handled in the standalone 456 | zend-expressive-zendviewrenderer package. 457 | 458 | ## 0.5.0 - 2015-10-10 459 | 460 | ### Added 461 | 462 | - Nothing. 463 | 464 | ### Deprecated 465 | 466 | - Nothing. 467 | 468 | ### Removed 469 | 470 | - Nothing. 471 | 472 | ### Fixed 473 | 474 | - [#3](https://github.com/zendframework/zend-expressive-skeleton/pull/3) updates 475 | the skeleton to use zendframework/zend-expressive 0.4.0. 476 | 477 | ## 0.4.0 - 2015-10-09 478 | 479 | First release as zend-expressive-skeleton. 480 | 481 | ### Added 482 | 483 | - Nothing. 484 | 485 | ### Deprecated 486 | 487 | - Nothing. 488 | 489 | ### Removed 490 | 491 | - Nothing. 492 | 493 | ### Fixed 494 | 495 | - Nothing. 496 | 497 | ## 0.3.0 - 2015-09-12 498 | 499 | ### Added 500 | 501 | - Use zend-expressive template factories. 502 | - Use the zend view url helper in the layout template. 503 | 504 | ### Deprecated 505 | 506 | - Nothing. 507 | 508 | ### Removed 509 | 510 | - Nothing. 511 | 512 | ### Fixed 513 | 514 | - Nothing. 515 | 516 | ## 0.2.0 - 2015-09-11 517 | 518 | ### Added 519 | 520 | - [#bbb2e60](https://github.com/xtreamwayz/expressive-composer-installer/commit/bbb2e607af23e3ae23f6a9c71eb97c3c651c0ca1) adds PHPUnit tests. 521 | - [#791c1c6](https://github.com/xtreamwayz/expressive-composer-installer/commit/791c1c63f324ca08d08e26375f3a356102bf2ad9) adds Whoops error handler. 522 | - [e1d8d7bf](https://github.com/xtreamwayz/expressive-composer-installer/commit/e1d8d7bf5d5e2f51863fa59a37d1963405743201) adds config caching in production mode. 523 | 524 | ### Deprecated 525 | 526 | - Nothing. 527 | 528 | ### Removed 529 | 530 | - Nothing. 531 | 532 | ### Fixed 533 | 534 | - Nothing. 535 | 536 | ## 0.1.1 - 2015-09-08 537 | 538 | ### Added 539 | 540 | - [#b4a0923](https://github.com/xtreamwayz/expressive-composer-installer/commit/b4a092386993227f8057d7ad4e0d9762659eefb0) adds support for Pimple 3.0.x. Still needs testing! 541 | 542 | ### Deprecated 543 | 544 | - Nothing. 545 | 546 | ### Removed 547 | 548 | - Nothing. 549 | 550 | ### Fixed 551 | 552 | - [#11](https://github.com/xtreamwayz/expressive-composer-installer/issues/11) fixes an issues where non stable packages are not being installed correctly. 553 | 554 | ## 0.1.0 - 2015-09-07 555 | 556 | Initial tagged release. 557 | 558 | ### Added 559 | 560 | - Everything. 561 | 562 | ### Deprecated 563 | 564 | - Nothing. 565 | 566 | ### Removed 567 | 568 | - Nothing. 569 | 570 | ### Fixed 571 | 572 | - Nothing. 573 | -------------------------------------------------------------------------------- /CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | The Zend Framework project adheres to [The Code Manifesto](http://codemanifesto.com) 4 | as its guidelines for contributor interactions. 5 | 6 | ## The Code Manifesto 7 | 8 | We want to work in an ecosystem that empowers developers to reach their 9 | potential — one that encourages growth and effective collaboration. A space that 10 | is safe for all. 11 | 12 | A space such as this benefits everyone that participates in it. It encourages 13 | new developers to enter our field. It is through discussion and collaboration 14 | that we grow, and through growth that we improve. 15 | 16 | In the effort to create such a place, we hold to these values: 17 | 18 | 1. **Discrimination limits us.** This includes discrimination on the basis of 19 | race, gender, sexual orientation, gender identity, age, nationality, technology 20 | and any other arbitrary exclusion of a group of people. 21 | 2. **Boundaries honor us.** Your comfort levels are not everyone’s comfort 22 | levels. Remember that, and if brought to your attention, heed it. 23 | 3. **We are our biggest assets.** None of us were born masters of our trade. 24 | Each of us has been helped along the way. Return that favor, when and where 25 | you can. 26 | 4. **We are resources for the future.** As an extension of #3, share what you 27 | know. Make yourself a resource to help those that come after you. 28 | 5. **Respect defines us.** Treat others as you wish to be treated. Make your 29 | discussions, criticisms and debates from a position of respectfulness. Ask 30 | yourself, is it true? Is it necessary? Is it constructive? Anything less is 31 | unacceptable. 32 | 6. **Reactions require grace.** Angry responses are valid, but abusive language 33 | and vindictive actions are toxic. When something happens that offends you, 34 | handle it assertively, but be respectful. Escalate reasonably, and try to 35 | allow the offender an opportunity to explain themselves, and possibly correct 36 | the issue. 37 | 7. **Opinions are just that: opinions.** Each and every one of us, due to our 38 | background and upbringing, have varying opinions. The fact of the matter, is 39 | that is perfectly acceptable. Remember this: if you respect your own 40 | opinions, you should respect the opinions of others. 41 | 8. **To err is human.** You might not intend it, but mistakes do happen and 42 | contribute to build experience. Tolerate honest mistakes, and don't hesitate 43 | to apologize if you make one yourself. 44 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | ## RESOURCES 4 | 5 | If you wish to contribute to Zend Framework, please be sure to 6 | read/subscribe to the following resources: 7 | 8 | - [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards) 9 | - [Contributor's Guide](CONTRIBUTING.md) 10 | - ZF Contributor's mailing list: 11 | Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html 12 | Subscribe: zf-contributors-subscribe@lists.zend.com 13 | - ZF Contributor's IRC channel: 14 | #zftalk.dev on Freenode.net 15 | 16 | If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-expressive-skeleton/issues/new). 17 | 18 | ## Reporting Potential Security Issues 19 | 20 | If you have encountered a potential security vulnerability, please **DO NOT** report it on the public 21 | issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead. 22 | We will work with you to verify the vulnerability and patch it as soon as possible. 23 | 24 | When reporting issues, please provide the following information: 25 | 26 | - Component(s) affected 27 | - A description indicating how to reproduce the issue 28 | - A summary of the security vulnerability and impact 29 | 30 | We request that you contact us via the email address above and give the project 31 | contributors a chance to resolve the vulnerability and issue a new release prior 32 | to any public exposure; this helps protect users and provides them with a chance 33 | to upgrade and/or update in order to protect their applications. 34 | 35 | For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc). 36 | 37 | ## RUNNING TESTS 38 | 39 | To run tests: 40 | 41 | - Clone the repository: 42 | 43 | ```console 44 | $ git clone git@github.com:zendframework/zend-expressive-skeleton.git 45 | $ cd zend-expressive-skeleton 46 | ``` 47 | 48 | - Install dependencies via composer: 49 | 50 | ```console 51 | $ composer install 52 | ``` 53 | 54 | **NOTE:** If you are wanting to test the installer itself, add the 55 | `--no-scripts` flag to the `composer install` command. 56 | 57 | If you don't have `curl` installed, you can also download `composer.phar` from 58 | https://getcomposer.org/: 59 | 60 | ```console 61 | $ curl -sS https://getcomposer.org/installer | php -- 62 | $ ln -s composer.phar composer 63 | ``` 64 | 65 | - Run the tests using the "test" command shipped in the `composer.json`: 66 | 67 | ```console 68 | $ composer test 69 | ``` 70 | 71 | You can turn on conditional tests with the `phpunit.xml` file. 72 | To do so: 73 | 74 | - Copy `phpunit.xml.dist` file to `phpunit.xml` 75 | - Edit `phpunit.xml` to enable any specific functionality you 76 | want to test, as well as to provide test values to utilize. 77 | 78 | ## Running Coding Standards Checks 79 | 80 | First, ensure you've installed dependencies via composer, per the previous 81 | section on running tests. 82 | 83 | To run CS checks only: 84 | 85 | ```console 86 | $ composer cs 87 | ``` 88 | 89 | To attempt to automatically fix common CS issues: 90 | 91 | 92 | ```console 93 | $ composer cs-fix 94 | ``` 95 | 96 | If the above fixes any CS issues, please re-run the tests to ensure 97 | they pass, and make sure you add and commit the changes after verification. 98 | 99 | ## Recommended Workflow for Contributions 100 | 101 | Your first step is to establish a public repository from which we can 102 | pull your work into the master repository. We recommend using 103 | [GitHub](https://github.com), as that is where the component is already hosted. 104 | 105 | 1. Setup a [GitHub account](http://github.com/), if you haven't yet 106 | 2. Fork the repository (http://github.com/zendframework/zend-expressive-skeleton) 107 | 3. Clone the canonical repository locally and enter it. 108 | 109 | ```console 110 | $ git clone git://github.com:zendframework/zend-expressive-skeleton.git 111 | $ cd zend-expressive-skeleton 112 | ``` 113 | 114 | 4. Add a remote to your fork; substitute your GitHub username in the command 115 | below. 116 | 117 | ```console 118 | $ git remote add {username} git@github.com:{username}/zend-expressive-skeleton.git 119 | $ git fetch {username} 120 | ``` 121 | 122 | ### Keeping Up-to-Date 123 | 124 | Periodically, you should update your fork or personal repository to 125 | match the canonical ZF repository. Assuming you have setup your local repository 126 | per the instructions above, you can do the following: 127 | 128 | 129 | ```console 130 | $ git checkout master 131 | $ git fetch origin 132 | $ git rebase origin/master 133 | # OPTIONALLY, to keep your remote up-to-date - 134 | $ git push {username} master:master 135 | ``` 136 | 137 | If you're tracking other branches -- for example, the "develop" branch, where 138 | new feature development occurs -- you'll want to do the same operations for that 139 | branch; simply substitute "develop" for "master". 140 | 141 | ### Working on a patch 142 | 143 | We recommend you do each new feature or bugfix in a new branch. This simplifies 144 | the task of code review as well as the task of merging your changes into the 145 | canonical repository. 146 | 147 | A typical workflow will then consist of the following: 148 | 149 | 1. Create a new local branch based off either your master or develop branch. 150 | 2. Switch to your new local branch. (This step can be combined with the 151 | previous step with the use of `git checkout -b`.) 152 | 3. Do some work, commit, repeat as necessary. 153 | 4. Push the local branch to your remote repository. 154 | 5. Send a pull request. 155 | 156 | The mechanics of this process are actually quite trivial. Below, we will 157 | create a branch for fixing an issue in the tracker. 158 | 159 | ```console 160 | $ git checkout -b hotfix/9295 161 | Switched to a new branch 'hotfix/9295' 162 | ``` 163 | 164 | ... do some work ... 165 | 166 | 167 | ```console 168 | $ git commit 169 | ``` 170 | 171 | ... write your log message ... 172 | 173 | 174 | ```console 175 | $ git push {username} hotfix/9295:hotfix/9295 176 | Counting objects: 38, done. 177 | Delta compression using up to 2 threads. 178 | Compression objects: 100% (18/18), done. 179 | Writing objects: 100% (20/20), 8.19KiB, done. 180 | Total 20 (delta 12), reused 0 (delta 0) 181 | To ssh://git@github.com/{username}/zend-expressive-skeleton.git 182 | b5583aa..4f51698 HEAD -> master 183 | ``` 184 | 185 | To send a pull request, you have two options. 186 | 187 | If using GitHub, you can do the pull request from there. Navigate to 188 | your repository, select the branch you just created, and then select the 189 | "Pull Request" button in the upper right. Select the user/organization 190 | "zendframework" as the recipient. 191 | 192 | If using your own repository - or even if using GitHub - you can use `git 193 | format-patch` to create a patchset for us to apply; in fact, this is 194 | **recommended** for security-related patches. If you use `format-patch`, please 195 | send the patches as attachments to: 196 | 197 | - zf-devteam@zend.com for patches without security implications 198 | - zf-security@zend.com for security patches 199 | 200 | #### What branch to issue the pull request against? 201 | 202 | Which branch should you issue a pull request against? 203 | 204 | - For fixes against the stable release, issue the pull request against the 205 | "master" branch. 206 | - For new features, or fixes that introduce new elements to the public API (such 207 | as new public methods or properties), issue the pull request against the 208 | "develop" branch. 209 | 210 | ### Branch Cleanup 211 | 212 | As you might imagine, if you are a frequent contributor, you'll start to 213 | get a ton of branches both locally and on your remote. 214 | 215 | Once you know that your changes have been accepted to the master 216 | repository, we suggest doing some cleanup of these branches. 217 | 218 | - Local branch cleanup 219 | 220 | ```console 221 | $ git branch -d 222 | ``` 223 | 224 | - Remote branch removal 225 | 226 | ```console 227 | $ git push {username} : 228 | ``` 229 | 230 | 231 | ## Conduct 232 | 233 | Please see our [CONDUCT.md](CONDUCT.md) to understand expected behavior when interacting with others in the project. 234 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Zend Technologies USA, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | - Neither the name of Zend Technologies USA, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expressive Skeleton and Installer 2 | 3 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-expressive-skeleton.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-expressive-skeleton) 4 | [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-expressive-skeleton/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-expressive-skeleton?branch=master) 5 | 6 | *Begin developing PSR-7 middleware applications in seconds!* 7 | 8 | [zend-expressive](https://github.com/zendframework/zend-expressive) builds on 9 | [zend-stratigility](https://github.com/zendframework/zend-stratigility) to 10 | provide a minimalist PSR-7 middleware framework for PHP with routing, DI 11 | container, optional templating, and optional error handling capabilities. 12 | 13 | This installer will setup a skeleton application based on zend-expressive by 14 | choosing optional packages based on user input as demonstrated in the following 15 | screenshot: 16 | 17 | ![screenshot-installer](https://cloud.githubusercontent.com/assets/459648/10410494/16bdc674-6f6d-11e5-8190-3c1466e93361.png) 18 | 19 | The user selected packages are saved into `composer.json` so that everyone else 20 | working on the project have the same packages installed. Configuration files and 21 | templates are prepared for first use. The installer command is removed from 22 | `composer.json` after setup succeeded, and all installer related files are 23 | removed. 24 | 25 | ## Getting Started 26 | 27 | Start your new Expressive project with composer: 28 | 29 | ```bash 30 | $ composer create-project zendframework/zend-expressive-skeleton 31 | ``` 32 | 33 | After choosing and installing the packages you want, go to the 34 | `` and start PHP's built-in web server to verify installation: 35 | 36 | ```bash 37 | $ composer serve 38 | ``` 39 | 40 | You can then browse to http://localhost:8080. 41 | 42 | > ### Setting a timeout 43 | > 44 | > Composer commands time out after 300 seconds (5 minutes). On Linux-based 45 | > systems, the `php -S` command that `composer server` spawns continues running 46 | > as a background process, but on other systems halts when the timeout occurs. 47 | > 48 | > If you want the server to live longer, you can use the 49 | > `COMPOSER_PROCESS_TIMEOUT` environment variable when executing `composer 50 | > serve` to extend the timeout. As an example, the following will extend it 51 | > to a full day: 52 | > 53 | > ```bash 54 | > $ COMPOSER_PROCESS_TIMEOUT=86400 composer serve 55 | > ``` 56 | 57 | ## Troubleshooting 58 | 59 | If the installer fails during the ``composer create-project`` phase, please go 60 | through the following list before opening a new issue. Most issues we have seen 61 | so far can be solved by `self-update` and `clear-cache`. 62 | 63 | 1. Be sure to work with the latest version of composer by running `composer self-update`. 64 | 2. Try clearing Composer's cache by running `composer clear-cache`. 65 | 66 | If neither of the above help, you might face more serious issues: 67 | 68 | - Info about the [zlib_decode error](https://github.com/composer/composer/issues/4121). 69 | - Info and solutions for [composer degraded mode](https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode). 70 | 71 | ## Skeleton Development 72 | 73 | This section applies only if you cloned this repo with `git clone`, not when you 74 | installed expressive with `composer create-project ...`. 75 | 76 | If you want to run tests against the installer, you need to clone this repo and 77 | setup all dependencies with composer. Make sure you **prevent composer running 78 | scripts** with `--no-scripts`, otherwise it will remove the installer and all 79 | tests. 80 | 81 | ```bash 82 | $ composer install --no-scripts 83 | $ composer test 84 | ``` 85 | 86 | Please note that the installer tests remove installed config files and templates 87 | before and after running the tests. 88 | 89 | Before contributing read [the contributing guide](CONTRIBUTING.md). 90 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ralfeggert/zend-expressive-tutorial", 3 | "description": "Zend\\Expressive tutorial based on the Zend\\Expressive skeleton", 4 | "type": "project", 5 | "homepage": "https://github.com/ralfeggert/zend-expressive-tutorial", 6 | "license": "BSD-3-CLAUSE", 7 | "authors": [ 8 | { 9 | "name": "Ralf Eggert", 10 | "homepage": "http://www.ralfeggert.de/" 11 | }, 12 | { 13 | "name": "Geert Eltink", 14 | "homepage": "https://xtreamwayz.com/" 15 | } 16 | ], 17 | "extra": { 18 | "branch-alias": { 19 | "dev-master": "1.0-dev", 20 | "dev-develop": "1.1-dev" 21 | } 22 | }, 23 | "require": { 24 | "php": "^5.5 || ^7.0", 25 | "roave/security-advisories": "dev-master", 26 | "zendframework/zend-expressive": "^1.0", 27 | "zendframework/zend-expressive-helpers": "^2.0", 28 | "zendframework/zend-stdlib": "^2.7 || ^3.0", 29 | "zendframework/zend-expressive-zendrouter": "^1.0", 30 | "zendframework/zend-servicemanager": "^2.7.3 || ^3.0", 31 | "zendframework/zend-expressive-zendviewrenderer": "^1.0", 32 | "zendframework/zend-component-installer": "^0.3.0", 33 | "mtymek/expressive-config-manager": "^0.4.0" 34 | }, 35 | "require-dev": { 36 | "phpunit/phpunit": "^4.8", 37 | "squizlabs/php_codesniffer": "^2.3", 38 | "filp/whoops": "^1.1 || ^2.0" 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "App\\": "src/App/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "AppTest\\": "test/AppTest/" 48 | } 49 | }, 50 | "scripts": { 51 | "check": [ 52 | "@cs", 53 | "@test" 54 | ], 55 | "cs-check": "phpcs", 56 | "cs-fix": "phpcbf", 57 | "serve": "php -S 0.0.0.0:8080 -t public/ public/index.php", 58 | "test": "phpunit --colors=always", 59 | "test-coverage": "phpunit --colors=always --coverage-clover clover.xml", 60 | "upload-coverage": "coveralls -v" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /config/autoload/.gitignore: -------------------------------------------------------------------------------- 1 | local.php 2 | *.local.php 3 | -------------------------------------------------------------------------------- /config/autoload/dependencies.global.php: -------------------------------------------------------------------------------- 1 | [ 11 | // Use 'invokables' for constructor-less services, or services that do 12 | // not require arguments to the constructor. Map a service name to the 13 | // class name. 14 | 'invokables' => [ 15 | // Fully\Qualified\InterfaceName::class => Fully\Qualified\ClassName::class, 16 | Helper\ServerUrlHelper::class => Helper\ServerUrlHelper::class, 17 | ], 18 | // Use 'factories' for services provided by callbacks/factory classes. 19 | 'factories' => [ 20 | Application::class => ApplicationFactory::class, 21 | Helper\UrlHelper::class => Helper\UrlHelperFactory::class, 22 | ], 23 | ], 24 | ]; 25 | -------------------------------------------------------------------------------- /config/autoload/local.php.dist: -------------------------------------------------------------------------------- 1 | true, 5 | 6 | 'config_cache_enabled' => false, 7 | ]; 8 | -------------------------------------------------------------------------------- /config/autoload/middleware-pipeline.global.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'factories' => [ 8 | Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class, 9 | Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class, 10 | ], 11 | ], 12 | // This can be used to seed pre- and/or post-routing middleware 13 | 'middleware_pipeline' => [ 14 | // An array of middleware to register. Each item is of the following 15 | // specification: 16 | // 17 | // [ 18 | // Required: 19 | // 'middleware' => 'Name or array of names of middleware services and/or callables', 20 | // Optional: 21 | // 'path' => '/path/to/match', // string; literal path prefix to match 22 | // // middleware will not execute 23 | // // if path does not match! 24 | // 'error' => true, // boolean; true for error middleware 25 | // 'priority' => 1, // int; higher values == register early; 26 | // // lower/negative == register last; 27 | // // default is 1, if none is provided. 28 | // ], 29 | // 30 | // While the ApplicationFactory ignores the keys associated with 31 | // specifications, they can be used to allow merging related values 32 | // defined in multiple configuration files/locations. This file defines 33 | // some conventional keys for middleware to execute early, routing 34 | // middleware, and error middleware. 35 | 'always' => [ 36 | 'middleware' => [ 37 | // Add more middleware here that you want to execute on 38 | // every request: 39 | // - bootstrapping 40 | // - pre-conditions 41 | // - modifications to outgoing responses 42 | Helper\ServerUrlMiddleware::class, 43 | ], 44 | 'priority' => 10000, 45 | ], 46 | 47 | 'routing' => [ 48 | 'middleware' => [ 49 | ApplicationFactory::ROUTING_MIDDLEWARE, 50 | Helper\UrlHelperMiddleware::class, 51 | // Add more middleware here that needs to introspect the routing 52 | // results; this might include: 53 | // - route-based authentication 54 | // - route-based validation 55 | // - etc. 56 | ApplicationFactory::DISPATCH_MIDDLEWARE, 57 | ], 58 | 'priority' => 1, 59 | ], 60 | 61 | 'error' => [ 62 | 'middleware' => [ 63 | // Add error middleware here. 64 | ], 65 | 'error' => true, 66 | 'priority' => -10000, 67 | ], 68 | ], 69 | ]; 70 | -------------------------------------------------------------------------------- /config/autoload/routes.global.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'invokables' => [ 6 | Zend\Expressive\Router\RouterInterface::class => Zend\Expressive\Router\ZendRouter::class, 7 | App\Action\PingAction::class => App\Action\PingAction::class, 8 | ], 9 | 'factories' => [ 10 | App\Action\HomePageAction::class => App\Action\HomePageFactory::class, 11 | ], 12 | ], 13 | 14 | 'routes' => [ 15 | [ 16 | 'name' => 'home', 17 | 'path' => '/', 18 | 'middleware' => App\Action\HomePageAction::class, 19 | 'allowed_methods' => ['GET'], 20 | ], 21 | [ 22 | 'name' => 'api.ping', 23 | 'path' => '/api/ping', 24 | 'middleware' => App\Action\PingAction::class, 25 | 'allowed_methods' => ['GET'], 26 | ], 27 | ], 28 | ]; 29 | -------------------------------------------------------------------------------- /config/autoload/templates.global.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'factories' => [ 6 | 'Zend\Expressive\FinalHandler' => 7 | Zend\Expressive\Container\TemplatedErrorHandlerFactory::class, 8 | 9 | Zend\Expressive\Template\TemplateRendererInterface::class => 10 | Zend\Expressive\ZendView\ZendViewRendererFactory::class, 11 | 12 | Zend\View\HelperPluginManager::class => 13 | Zend\Expressive\ZendView\HelperPluginManagerFactory::class, 14 | ], 15 | ], 16 | 17 | 'templates' => [ 18 | 'layout' => 'layout/default', 19 | 'map' => [ 20 | 'layout/default' => 'templates/layout/default.phtml', 21 | 'error/error' => 'templates/error/error.phtml', 22 | 'error/404' => 'templates/error/404.phtml', 23 | ], 24 | 'paths' => [ 25 | 'app' => ['templates/app'], 26 | 'layout' => ['templates/layout'], 27 | 'error' => ['templates/error'], 28 | ], 29 | ], 30 | 31 | 'view_helpers' => [ 32 | // zend-servicemanager-style configuration for adding view helpers: 33 | // - 'aliases' 34 | // - 'invokables' 35 | // - 'factories' 36 | // - 'abstract_factories' 37 | // - etc. 38 | ], 39 | ]; 40 | -------------------------------------------------------------------------------- /config/autoload/zend-expressive.global.php: -------------------------------------------------------------------------------- 1 | false, 5 | 6 | 'config_cache_enabled' => false, 7 | 8 | 'zend-expressive' => [ 9 | 'error_handler' => [ 10 | 'template_404' => 'error::404', 11 | 'template_error' => 'error::error', 12 | ], 13 | ], 14 | ]; 15 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | getMergedConfig()); 15 | -------------------------------------------------------------------------------- /config/container.php: -------------------------------------------------------------------------------- 1 | configureServiceManager($container); 12 | 13 | // Inject config 14 | $container->setService('config', $config); 15 | 16 | return $container; 17 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | html -------------------------------------------------------------------------------- /doc/book/images/install-zend-db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/install-zend-db.png -------------------------------------------------------------------------------- /doc/book/images/install-zend-hydrator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/install-zend-hydrator.png -------------------------------------------------------------------------------- /doc/book/images/install-zend-inputfilter-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/install-zend-inputfilter-form.png -------------------------------------------------------------------------------- /doc/book/images/installer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/installer.png -------------------------------------------------------------------------------- /doc/book/images/screen-after-installation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/screen-after-installation.png -------------------------------------------------------------------------------- /doc/book/images/screen-album-create-form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/screen-album-create-form.png -------------------------------------------------------------------------------- /doc/book/images/screen-album-delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/screen-album-delete.png -------------------------------------------------------------------------------- /doc/book/images/screen-album-list-links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/screen-album-list-links.png -------------------------------------------------------------------------------- /doc/book/images/screen-album-list-no-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/screen-album-list-no-data.png -------------------------------------------------------------------------------- /doc/book/images/screen-album-list-with-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/screen-album-list-with-data.png -------------------------------------------------------------------------------- /doc/book/images/screen-album-update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/doc/book/images/screen-album-update.png -------------------------------------------------------------------------------- /doc/book/part1.md: -------------------------------------------------------------------------------- 1 | # Part 1: Setup the application 2 | 3 | This tutorial provides an introduction to Expressive, and building 4 | [PSR-7](http://www.php-fig.org/psr/psr-7/) middleware applications. You will 5 | build a simple database driven application step-by-step, which you can then use 6 | as a starting point for your own applications. 7 | 8 | ## Create a new project with the installer 9 | 10 | To begin, we will create a new project by using 11 | [Composer](https://getcomposer.org). 12 | 13 | > ### Get Composer 14 | > 15 | > If you haven't already, install Composer, per the instructions 16 | > [on their website](https://getcomposer.org/doc/00-intro.md#installation-linux-unix-osx). 17 | 18 | Please run the following command: 19 | 20 | ```bash 21 | $ composer create-project zendframework/zend-expressive-skeleton zend-expressive-tutorial 22 | ``` 23 | 24 | The installer will prompt you with several questions, asking you 25 | to choose packages to install. These include the following: 26 | 27 | - `Minimal skeleton? (no default middleware, templates or assets; configuration 28 | only)`: For the tutorial, please choose `n` (the default selection) for a full 29 | skeleton. 30 | - `Which router do you want to use?`: please choose `3`, for the Zend Router. 31 | - `Which container do you want to use for dependency injection?`: please choose 32 | `3` (the default selection), for zend-servicemanager. 33 | - `Which template engine do you want to use?`: please choose `3`, for Zend View. 34 | - `Which error handler do you want to use during development?`: Please choose 35 | `1` (the default selection), for Whoops. 36 | 37 | The output on your screen should look like this: 38 | 39 | ![Zend\Expressive installer](images/installer.png) 40 | 41 | Once complete, enter the project directory: 42 | 43 | ```bash 44 | $ cd zend-expressive-tutorial 45 | ``` 46 | 47 | You can now startup PHP's [built-in web server](http://php.net/manual/en/features.commandline.webserver.php); 48 | the Expressive skeleton provides a short-cut for it via Composer: 49 | 50 | ```bash 51 | $ composer serve 52 | ``` 53 | 54 | > Server timeout 55 | > 56 | > By default composer will terminate with a `ProcessTimedOutException` after 57 | > 300 seconds (5 minutes). If you want it to run longer, you can alter the timeout 58 | > via the `COMPOSER_PROCESS_TIMEOUT` environment variable: 59 | > 60 | > ```bash 61 | > $ export COMPOSER_PROCESS_TIMEOUT=86400 62 | > $ composer serve 63 | > # or 64 | > $ COMPOSER_PROCESS_TIMEOUT=86400 composer serve 65 | > ``` 66 | 67 | This starts up a web server on localhost port 8080; browse to 68 | [http://localhost:8080/](http://localhost:8080/) to see if your 69 | application responds correctly! 70 | 71 | ![Screenshot after installation](images/screen-after-installation.png) 72 | 73 | ## Add the component installer and the config manager 74 | 75 | In the next step you should add the `Zend\ComponentInstaller` and the 76 | `Zend\Expressive\ConfigManager` via Composer to ease the installation of 77 | other Zend Framework components. 78 | 79 | The `Zend\ComponentInstaller` is a plugin for the Composer which helps you 80 | to activate the configuration provided by `ConfigProvider` classes in Zend 81 | Framework components. When you require new components it activates them 82 | for you. Read more at the official 83 | [`Zend\ComponentInstaller` documentation](https://docs.zendframework.com/zend-component-installer/). 84 | 85 | To install the `Zend\ComponentInstaller` just require it with the Composer: 86 | 87 | ```bash 88 | $ composer require zendframework/zend-component-installer 89 | ``` 90 | 91 | The `Zend\Expressive\ConfigManager` is a lightweight library for 92 | collecting and merging configuration from different sources. It is designed 93 | for `Zend\Expressive` applications, but it can work with any PHP project. 94 | 95 | To install the `Zend\Expressive\ConfigManager` just require it with the 96 | Composer: 97 | 98 | ```bash 99 | $ composer require mtymek/expressive-config-manager 100 | ``` 101 | 102 | Finally, you need to change the configuration of your `Zend\Expressive` 103 | application to use the `Zend\Expressive\ConfigManager`. For this you need 104 | to overwrite the current content of the `/config/config.php` file with the 105 | following code. 106 | 107 | 108 | ```php 109 | getMergedConfig()); 123 | ``` 124 | 125 | Please note: normally you only need to add the `PhpFileProvider`. But since 126 | the components `Zend\Filter`, `Zend\I18n`, `Zend\Router` and 127 | `Zend\Validator` have already been installed before the installation of 128 | the `Zend\Expressive\ConfigManager` you need to add them manually here. 129 | 130 | If you browse to [http://localhost:8080/](http://localhost:8080/) again 131 | your application should still work. 132 | 133 | ## Compare with example repository branch `part1` 134 | 135 | You can easily compare your code with the example repository when looking 136 | at the branch `part1`. If you want you can even clone it and have a deeper 137 | look. 138 | 139 | ```bash 140 | $ git clone https://github.com/RalfEggert/zend-expressive-tutorial.git 141 | $ cd zend-expressive-tutorial 142 | $ git checkout -b part1 origin/part1 143 | ``` 144 | 145 | Or view it online: 146 | 147 | - [https://github.com/RalfEggert/zend-expressive-tutorial/tree/part1](https://github.com/RalfEggert/zend-expressive-tutorial/tree/part1) 148 | 149 | **Attention:** Just a last note regarding the `composer.json` file in your project. The 150 | versions in your `require` section could differ from the versions in the 151 | `require` section of the repository. Versions get updated frequently and 152 | this tutorial and the corresponding code will not be updated for every 153 | version of every component. The updates will be done if there are bigger 154 | changes like new major versions for any component. 155 | -------------------------------------------------------------------------------- /doc/book/part2.md: -------------------------------------------------------------------------------- 1 | # Part 2: Album list middleware 2 | 3 | We will now setup a new middleware to show the album list. We will not have any 4 | data to display just yet, but the exercise will demonstrate writing your first 5 | middleware. 6 | 7 | ## Create the album list middleware 8 | 9 | To begin, we will need to create the new path `src/Album/Action/`, and place a 10 | new `AlbumListAction.php` file within it. 11 | 12 | To create the path: 13 | 14 | ```bash 15 | $ mkdir -p src/Album/Action 16 | ``` 17 | 18 | or use whatever tools you are comfortable with. 19 | 20 | Now create the file `src/Album/Action/AlbumListAction.php`, with the following 21 | contents: 22 | 23 | ```php 24 | template = $template; 45 | } 46 | 47 | /** 48 | * @param ServerRequestInterface $request 49 | * @param ResponseInterface $response 50 | * @param callable|null $next 51 | * @return HtmlResponse 52 | */ 53 | public function __invoke( 54 | ServerRequestInterface $request, 55 | ResponseInterface $response, 56 | callable $next = null 57 | ) { 58 | $data = []; 59 | 60 | return new HtmlResponse( 61 | $this->template->render('album::list', $data) 62 | ); 63 | } 64 | } 65 | ``` 66 | 67 | Since the `AlbumListAction` needs to render a template, it depends on the 68 | template renderer; we model that by having a `TemplateRendererInterface 69 | $template` argument in the class constructor. 70 | 71 | The `__invoke()` method is the middleware itself, and it creates and returns an 72 | `HtmlResponse`. The injected template renderer is used to create the content for 73 | the response, and renders our album list template. Since we have no data to 74 | output yet, an empty array is passed to the renderer. 75 | 76 | > ### Interfaces and classes used 77 | > 78 | > Some of the interfaces and classes referenced in the above example may look 79 | > unfamiliar; below are details on each. 80 | > 81 | > - The `Psr\Http\Message` namespace contains a set of standardized HTTP 82 | > message interfaces which are also known as [PSR-7](http://www.php-fig.org/psr/psr-7/). 83 | > Many PHP frameworks and open source projects consume these interfaces within 84 | > their projects, as they provide a convenient and interoperable abstraction 85 | > around HTTP messages. 86 | > 87 | > - The component [`Zend\Diactoros`](https://github.com/zendframework/zend-diactoros) 88 | > contains a PSR-7 implementation provided by the Zend Framework project, and 89 | > is the default implementation used by Expressive. 90 | > 91 | > - `HtmlResponse` is a convenience class for creating HTTP responses with HTML 92 | > content. By default, these responses have a 200 HTTP status code and a 93 | > Content-Type set to `text/html`. 94 | > 95 | > Note that the `HtmlResponse` class also accepts a status code and headers as 96 | > additional arguments. You can seed your instance with the status code and 97 | > headers present in the response instance passed to the middleware if 98 | > desired: 99 | > 100 | > ```php 101 | > return new HtmlResponse( 102 | > $this->template->render('album::list', $data), 103 | > $response->getStatusCode(), 104 | > $response->getHeaders() 105 | > ); 106 | > ``` 107 | 108 | > ### Middleware typehints 109 | > 110 | > Most examples in this tutorial will use typehinting for the middleware: 111 | > 112 | > ```php 113 | > public function __invoke( 114 | > ServerRequestInterface $request, 115 | > ResponseInterface $response, 116 | > callable $next = null 117 | > ) 118 | > ``` 119 | > 120 | > This can be somewhat cumbersome and repetitious when writing your code. As 121 | > such, you will often see middleware examples that omit the typehints: 122 | > 123 | > ```php 124 | > public function __invoke($request, $response, $next) 125 | > ``` 126 | > 127 | > When you see such code, keep the typehints from the previous example in mind. 128 | 129 | ## Create a factory for the album list middleware 130 | 131 | In order to work, the `AlbumListAction` requires a `TemplateRendererInterface` 132 | instance. How can we ensure one is injected? Create a factory, and inform the 133 | container about it! 134 | 135 | At this time, create the file `AlbumListFactory.php` within 136 | the same directory as the `AlbumListAction` class file, with the following 137 | contents: 138 | 139 | ```php 140 | get(TemplateRendererInterface::class); 155 | return new AlbumListAction($template); 156 | } 157 | } 158 | ``` 159 | 160 | The `__invoke()` method defines a factory for creating and returning an 161 | `AlbumListAction` instance. Internally, it fetches a template renderer instance 162 | from the DI container using the interface name, and the returned value is passed 163 | to the `AlbumListAction` constructor. 164 | 165 | Projects can assign their desired template renderer implementation to the 166 | service named after the interface, allowing the ability to swap implementations 167 | and ensure interoperability. 168 | 169 | We can now use this factory to create an `AlbumListAction` instance. 170 | 171 | > ### container-interop 172 | > 173 | > `Interop\Container\ContainerInterface` is provided by the [container-interop] 174 | > (https://github.com/container-interop/container-interop) package, 175 | > which tries to standardize features in container objects 176 | > (service locators, dependency injection containers, etc.) to achieve 177 | > interoperability. 178 | 179 | > ### Factory typehints 180 | > 181 | > Most examples in this tutorial will use typehinting for factories: 182 | > 183 | > ```php 184 | > public function __invoke(ContainerInterface $container) 185 | > ``` 186 | > 187 | > For simplicity, and interoperability, many developers omit the typehint: 188 | > 189 | > ```php 190 | > public function __invoke($container) 191 | > ``` 192 | > 193 | > This is particularly true for users of zend-servicemanager; that project, prior to the 194 | > 2.6.0 release, did not implement `ContainerInterface`, though its signature 195 | > was compatible. As such, omitting the typehint allowed re-use of factories. 196 | > 197 | > When you see such code, keep the typehints from the first example in mind. 198 | 199 | ## Create a template for listing albums 200 | 201 | We now need a template file for the album list. First, create 202 | the path `templates/album/` in the root of the project: 203 | 204 | ```bash 205 | $ mkdir -p templates/album/ 206 | ``` 207 | 208 | (or use whatever filesystem tools you're familiar with) 209 | 210 | Now, place a new `list.phtml` file within that path, with the following 211 | contents: 212 | 213 | ```php 214 | headTitle('Albums'); ?> 215 | 216 |
217 |

Album list

218 |
219 | ``` 220 | 221 | This template sets the title of the page, and prints the heading within a 222 | div that is styled by [Bootstrap](http://getbootstrap.com/). Please note, 223 | that there is no echo needed for the `$this->headTitle()` call since the 224 | output of the page title is done within the layout file 225 | (`templates/layout/default.phtml`). 226 | 227 | ## Tell the application about the middleware and template 228 | 229 | Currently, our Expressive application is unaware of the new action and template. 230 | Let's route our middleware and notify zend-view of our template. 231 | 232 | Create a file named `album.global.php` file in the directory `config/autoload/`, 233 | with the following contents: 234 | 235 | ```php 236 | [ 239 | 'factories' => [ 240 | Album\Action\AlbumListAction::class => Album\Action\AlbumListFactory::class, 241 | ], 242 | ], 243 | 244 | 'routes' => [ 245 | [ 246 | 'name' => 'album', 247 | 'path' => '/album', 248 | 'middleware' => Album\Action\AlbumListAction::class, 249 | 'allowed_methods' => ['GET'], 250 | ], 251 | ], 252 | 253 | 'templates' => [ 254 | 'paths' => [ 255 | 'album' => [ 'templates/album' ], 256 | ], 257 | ], 258 | ]; 259 | ``` 260 | 261 | - Within the `dependencies` configuration section you can configure the 262 | DI container (zend-servicemanager in our project). We use the class name of 263 | the new `AlbumListAction` as the service identifier, and the class name of the 264 | `AlbumListFactory` as the factory that will return an instance for that 265 | service. 266 | 267 | - Within the `routes` configuration section you can map path specifications to 268 | middleware. The configuration above defines the route named `album`, with the 269 | path `/album`, and maps it to our new `AlbumListAction` middleware. It also 270 | restricts access to GET requests for this route. 271 | 272 | - Within the `templates` configuration section, we configure the paths 273 | for the templates and map them to virtual namespaces. In the above, we've 274 | mapped the template namespace `album` to the path `templates/album/`. 275 | This allows the template `album::list` to map to the template file 276 | `templates/album/list.phtml`. 277 | 278 | ## Provide navigation 279 | 280 | How will users know the new page exists? Let's add a link to the new page within 281 | the menu. 282 | 283 | Open the `templates/layout/default.phtml` file and add the link to the album 284 | list by using the name of the route we configured in the previous section: 285 | 286 | ``` 287 | 288 |
289 | 304 |
305 | 306 | 307 | ``` 308 | 309 | ## Write tests 310 | 311 | Development should be accompanied by tests. Let's create some initial tests for 312 | our middleware. 313 | 314 | First, create the directory `test/AlbumTest/Action/`: 315 | 316 | ```bash 317 | $ mkdir -p test/AlbumTest/Action/ 318 | ``` 319 | 320 | (or use the filesystem tools you are familiar with) 321 | 322 | Now create the file `AlbumListActionTest.php` in this new path. This test case 323 | should test the action we just created, and specifically that it returns an 324 | instance of `Zend\Diactoros\Response\HtmlResponse` with expected content. 325 | 326 | Add the following content to the file `test/AlbumTest/Action/AlbumListActionTest.php`: 327 | 328 | ```php 329 | request = $this->prophesize(ServerRequestInterface::class); 362 | $this->response = $this->prophesize(ResponseInterface::class); 363 | 364 | $this->next = function () { 365 | }; 366 | } 367 | 368 | /** 369 | * Test if action renders the album list 370 | */ 371 | public function testActionRendersAlbumListTemplate() 372 | { 373 | $renderer = $this->prophesize(TemplateRendererInterface::class); 374 | $renderer 375 | ->render('album::list', []) 376 | ->shouldBeCalled() 377 | ->willReturn('BODY'); 378 | 379 | $action = new AlbumListAction($renderer->reveal()); 380 | 381 | $response = $action( 382 | $this->request->reveal(), 383 | $this->response->reveal(), 384 | $this->next 385 | ); 386 | 387 | $this->assertInstanceOf(HtmlResponse::class, $response); 388 | $this->assertEquals('BODY', $response->getBody()); 389 | } 390 | } 391 | ``` 392 | 393 | While it may seem unnecessary, we will also create a test for the factory. This 394 | ensures that as you make changes to your class, dependencies, or instantiation, 395 | you don't introduce bugs! 396 | 397 | Create the file `AlbumListFactoryTest.php` in the same path as the previous 398 | test, and write a test verifying the factory returns an `AlbumListAction` 399 | instance: 400 | 401 | ```php 402 | container = $this->prophesize(ContainerInterface::class); 424 | } 425 | 426 | /** 427 | * Test if factory returns the correct action 428 | */ 429 | public function testFactoryReturnsAlbumListAction() 430 | { 431 | $this->container 432 | ->get(TemplateRendererInterface::class) 433 | ->willReturn( 434 | $this->prophesize(TemplateRendererInterface::class)->reveal() 435 | ); 436 | 437 | $factory = new AlbumListFactory(); 438 | $this->assertTrue($factory instanceof AlbumListFactory); 439 | 440 | $action = $factory($this->container->reveal()); 441 | $this->assertTrue($action instanceof AlbumListAction); 442 | } 443 | } 444 | ``` 445 | 446 | ## Add autoloading 447 | 448 | In order to use the new classes we've created, we need to ensure our autoloader 449 | can find them! We can do this in the `composer.json` file by adding the new 450 | `Album` namespace and mapping it to the `src/Album/` path. Open that file, and 451 | update the `autoload.psr4` section to read as follows: 452 | 453 | ```javascript 454 | { 455 | "autoload": { 456 | "psr-4": { 457 | "App\\": "src/App/", 458 | "Album\\": "src/Album/" 459 | } 460 | }, 461 | } 462 | ``` 463 | 464 | You will also need to map the `AlbumTest` namespace to the `test/AlbumTest/` 465 | path; however, this only needs to be done for *development*, and, as such, we'll 466 | add the entry to the `autoload-dev.psr4` section of the `composer.json`: 467 | 468 | ```javascript 469 | { 470 | "autoload-dev": { 471 | "psr-4": { 472 | "AppTest\\": "test/AppTest/", 473 | "AlbumTest\\": "test/AlbumTest/" 474 | } 475 | }, 476 | } 477 | ``` 478 | 479 | Note: do not add or remove anything else! When you get done, run `composer 480 | validate` to ensure the changes you made are still valid. 481 | 482 | Adding the entry only tells Composer about the autoloading, but does not update 483 | the autoloader. To do that, run the following command: 484 | 485 | ```bash 486 | $ composer dump-autoload 487 | ``` 488 | 489 | ## Setup PHPUnit 490 | 491 | Setting up [PHPUnit](https://phpunit.de/) for testing is quite simple. Edit the 492 | `phpunit.xml.dist` file in the project root, and add the album test directory to 493 | the test suite: 494 | 495 | ```xml 496 | 497 | 498 | 499 | ./test/AppTest 500 | 501 | 502 | ./test/AlbumTest 503 | 504 | 505 | 506 | 507 | 508 | src 509 | 510 | 511 | 512 | ``` 513 | 514 | To run the tests, execute the following from the project root: 515 | 516 | ``` 517 | $ phpunit 518 | ``` 519 | 520 | ## Browse to the album list 521 | 522 | Now that we have verified programmatically what we've written, let's see what we 523 | get in the browser! 524 | 525 | If you do not have the built-in web server running, fire it up now: 526 | 527 | ```bash 528 | $ composer serve 529 | ``` 530 | 531 | Now browse to [http://localhost:8080/album](http://localhost:8080/album) to see if the 532 | album page was setup properly. 533 | 534 | ![Screenshot of album list with no data](images/screen-album-list-no-data.png) 535 | 536 | ## Compare with example repository branch `part2` 537 | 538 | You can compare your code with the example repository when looking at the branch 539 | `part2`. If you want you can even clone it and have a deeper look. 540 | 541 | - [https://github.com/RalfEggert/zend-expressive-tutorial/tree/part2](https://github.com/RalfEggert/zend-expressive-tutorial/tree/part2) 542 | -------------------------------------------------------------------------------- /doc/book/part3.md: -------------------------------------------------------------------------------- 1 | # Part 3: Model and database 2 | 3 | In this part of the tutorial, we will setup a database and implement the 4 | model layer for our application. At the end of this chapter, the album list 5 | page will show the data from the database. 6 | 7 | ## Setup the database and the database connection 8 | 9 | First, we need to setup the database. We will use MySQL for this tutorial. 10 | 11 | Create a new database called `album-tutorial`, and then run the following SQL 12 | statements to create the `album` table, along with some test data. 13 | 14 | ```sql 15 | CREATE TABLE album ( 16 | id int(11) NOT NULL auto_increment, 17 | artist varchar(100) NOT NULL, 18 | title varchar(100) NOT NULL, 19 | PRIMARY KEY (id) 20 | ); 21 | INSERT INTO album (artist, title) 22 | VALUES ('The Military Wives', 'In My Dreams'); 23 | INSERT INTO album (artist, title) 24 | VALUES ('Adele', '21'); 25 | INSERT INTO album (artist, title) 26 | VALUES ('Bruce Springsteen', 'Wrecking Ball (Deluxe)'); 27 | INSERT INTO album (artist, title) 28 | VALUES ('Lana Del Rey', 'Born To Die'); 29 | INSERT INTO album (artist, title) 30 | VALUES ('Gotye', 'Making Mirrors'); 31 | ``` 32 | 33 | ## Install `Zend\Db` component 34 | 35 | Next, we'll add the [zend-db](https://github.com/zendframework/zend-db) 36 | component to the application, using composer: 37 | 38 | ``` 39 | $ composer require zendframework/zend-db 40 | ``` 41 | 42 | When you run this installation via Composer the `Zend\ComponentInstaller` 43 | steps in here now and asks you if you want to inject the 44 | `Zend\Db\ConfigProvider` into your config file. You should select it with 45 | the choice of `1` and also remember your decision with `y`. It should look 46 | like this: 47 | 48 | ![Install Zend\Db component](images/install-zend-db.png) 49 | 50 | Please note that your `/config/config.php` should be updated as well by 51 | adding the `Zend\Db\ConfigProvider`: 52 | 53 | ```php 54 | 55 | use Zend\Expressive\ConfigManager\ConfigManager; 56 | use Zend\Expressive\ConfigManager\PhpFileProvider; 57 | 58 | $configManager = new ConfigManager([ 59 | \Zend\Db\ConfigProvider::class, 60 | Zend\Filter\ConfigProvider::class, 61 | Zend\I18n\ConfigProvider::class, 62 | Zend\Router\ConfigProvider::class, 63 | Zend\Validator\ConfigProvider::class, 64 | new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), 65 | ]); 66 | 67 | return new ArrayObject($configManager->getMergedConfig()); 68 | ``` 69 | 70 | To configure database access, create the file 71 | `/config/autoload/database.global.php` with the following contents: 72 | 73 | ```php 74 | [ 77 | 'driver' => 'pdo', 78 | 'dsn' => 'mysql:dbname=album-tutorial;host=localhost;charset=utf8', 79 | 'user' => 'album', 80 | 'pass' => 'album', 81 | ], 82 | ]; 83 | ``` 84 | 85 | The `db` configuration section defines the database connection. We will 86 | use the PDO driver with a MySQL database, and the database table and user 87 | we created above. 88 | 89 | > ### Store credentials in `*.local.php` files 90 | > 91 | > The above example stores the database credentials in a "global" configuration 92 | > file. **DO NOT DO THIS.** 93 | > 94 | > In your global configuration files, put in empty credentials. Then, in a file 95 | > named `database.local.php`, add the same structure, and provide the 96 | > credentials: 97 | > 98 | > ```php 99 | > return [ 101 | > 'db' => [ 102 | > 'driver' => 'pdo', 103 | > 'dsn' => 'mysql:dbname=album-tutorial;host=localhost;charset=utf8', 104 | > 'user' => 'album', 105 | > 'pass' => 'album', 106 | > ], 107 | > ]; 108 | > ``` 109 | > 110 | > "local" configuration files are merged *after* global configuration files, 111 | > which means they will have precedence. Additionally, they are omitted from 112 | > version control by default (via a `.gitignore` rule in the project root), 113 | > ensuring they will not be checked in to your repository. This also allows you 114 | > to have separate credentials and configuration per location where you deploy, 115 | > whether that's development, staging, QA, or production. 116 | 117 | At this point, we have setup the database, and provided a database adapter to 118 | our application. 119 | 120 | ## Create an album entity 121 | 122 | To represent the data of the albums, we will create an entity class. Create 123 | the directory `src/Album/Model/Entity/`; under it, create an `AlbumEntity.php` 124 | file with the following contents: 125 | 126 | ```php 127 | id; 156 | } 157 | 158 | /** 159 | * @return string 160 | */ 161 | public function getArtist() 162 | { 163 | return $this->artist; 164 | } 165 | 166 | /** 167 | * @return string 168 | */ 169 | public function getTitle() 170 | { 171 | return $this->title; 172 | } 173 | 174 | /** 175 | * @param array $array 176 | */ 177 | public function exchangeArray(array $array) 178 | { 179 | foreach ($array as $key => $value) { 180 | $setter = 'set' . ucfirst($key); 181 | 182 | if (method_exists($this, $setter)) { 183 | $this->{$setter}($value); 184 | } 185 | } 186 | } 187 | 188 | /** 189 | * @return array 190 | */ 191 | public function getArrayCopy() 192 | { 193 | $data = []; 194 | 195 | foreach (get_object_vars($this) as $key => $value) { 196 | $data[$key] = $value; 197 | } 198 | 199 | return $data; 200 | } 201 | 202 | 203 | /** 204 | * @param int $id 205 | */ 206 | private function setId($id) 207 | { 208 | $id = (int) $id; 209 | 210 | if ($id <= 0) { 211 | throw new DomainException( 212 | 'Album id must be a positive integer!' 213 | ); 214 | } 215 | 216 | $this->id = $id; 217 | } 218 | 219 | /** 220 | * @param string $artist 221 | */ 222 | private function setArtist($artist) 223 | { 224 | $artist = (string) $artist; 225 | 226 | if (empty($artist) || strlen($artist) > 100) { 227 | throw new DomainException( 228 | 'Album artist must be between 1 and 100 chars!' 229 | ); 230 | } 231 | 232 | $this->artist = $artist; 233 | } 234 | 235 | /** 236 | * @param string $title 237 | */ 238 | private function setTitle($title) 239 | { 240 | $title = (string) $title; 241 | 242 | if (empty($title) || strlen($title) > 100) { 243 | throw new DomainException( 244 | 'Album title must be between 1 and 100 chars!' 245 | ); 246 | } 247 | 248 | $this->title = $title; 249 | } 250 | } 251 | ``` 252 | 253 | There are a few things to note: 254 | 255 | - `AlbumEntity` implements `Zend\Stdlib\ArraySerializableInterface`, 256 | which provides the methods `exchangeArray()` and `getArrayCopy()`, allowing 257 | array de/serialization. This allows us to bind the entity to a form, as well 258 | as to handle the exchange of the data coming from the database. 259 | 260 | - The three private properties only allow access via the implemented 261 | methods. To get the values for `id`, `artist`, and `title`, you need to use the 262 | four getter methods. To change the data, you need to use the 263 | `exchangeArray()` method. 264 | 265 | - Within the `exchangeArray()` method, the injected array is looped. For 266 | each key, we build a setter method name and check if the method exists. 267 | The value is only set for this key if that check was successful. 268 | 269 | - Within the `getArrayCopy()` method, we look through all the properties of 270 | the current object and build a `$data` array that gets returned at the 271 | end. 272 | 273 | - Each property has a private setter method which casts and validates the value, 274 | raising an exception for invalid data. 275 | 276 | ## Create a storage interface 277 | 278 | To access the data from the database table, we will use the 279 | `Zend\Db\TableGateway` subcomponent. But before we do that, we have a little 280 | preparation to do first. 281 | 282 | If we use `Zend\Db\TableGateway` directly, we're binding our model to a specific 283 | data access layer, and more generally to relational databases. This means that 284 | if any changes happen to the `Zend\Db\TableGateway` implementation, we will need 285 | to change our code; if we decide to move to a NoSQL database later, we will need 286 | to change our code. 287 | 288 | To prevent the need for such changes, we will create a storage interface 289 | modeling our low layer data access needs. 290 | 291 | Create the path `src/Album/Model/Storage/` and then create the file 292 | `AlbumStorageInterface.php` beneath it. This interface defines methods for 293 | reading a list of albums, reading a single album, inserting an album, updating 294 | an album, and deleting albums. 295 | 296 | ```php 297 | getMergedConfig()); 387 | ``` 388 | 389 | ## Create a table gateway 390 | 391 | A [table data gateway](http://martinfowler.com/eaaCatalog/tableDataGateway.html) 392 | represents the data of a single table in your database, and allows reading and 393 | writing access to this data. 394 | [`Zend\Db\TableGateway`](http://framework.zend.com/manual/current/en/modules/zend.db.table-gateway.html) 395 | implements this pattern. 396 | 397 | Because storage is not part of the domain model and implementation can be 398 | swapped (because we defined a storage interface!), we'll place our table gateway 399 | in a separate path. Create the directory `src/Album/Db/`, and place the 400 | `AlbumTableGateway.php` file in it. Our table gateway will implement the 401 | `AlbumStorageInterface` interface defined in the previous section, and extend 402 | `Zend\Db\TableGateway\TableGateway`. 403 | 404 | ```php 405 | getSql()->select(); 431 | 432 | $collection = []; 433 | 434 | /** @var AlbumEntity $entity */ 435 | foreach ($this->selectWith($select) as $entity) { 436 | $collection[$entity->getId()] = $entity; 437 | } 438 | 439 | return $collection; 440 | } 441 | 442 | /** 443 | * {@inheritDoc} 444 | */ 445 | public function fetchAlbumById($id) 446 | { 447 | $select = $this->getSql()->select(); 448 | $select->where->equalTo('id', $id); 449 | 450 | return $this->selectWith($select)->current(); 451 | } 452 | 453 | /** 454 | * {@inheritDoc} 455 | */ 456 | public function insertAlbum(AlbumEntity $album) 457 | { 458 | $insertData = $album->getArrayCopy(); 459 | 460 | $insert = $this->getSql()->insert(); 461 | $insert->values($insertData); 462 | 463 | return $this->insertWith($insert) > 0; 464 | } 465 | 466 | /** 467 | * {@inheritDoc} 468 | */ 469 | public function updateAlbum(AlbumEntity $album) 470 | { 471 | $updateData = $album->getArrayCopy(); 472 | 473 | $update = $this->getSql()->update(); 474 | $update->set($updateData); 475 | $update->where->equalTo('id', $album->getId()); 476 | 477 | return $this->updateWith($update) > 0; 478 | } 479 | 480 | /** 481 | * {@inheritDoc} 482 | */ 483 | public function deleteAlbum(AlbumEntity $album) 484 | { 485 | $delete = $this->getSql()->delete(); 486 | $delete->where->equalTo('id', $album->getId()); 487 | 488 | return $this->deleteWith($delete) > 0; 489 | } 490 | } 491 | ``` 492 | 493 | Notes: 494 | 495 | - The constructor defines parameters for the database adapter and a 496 | pre-configured result set prototype. This prototype is used for all the 497 | selects from the database to represent the data. Within the constructor, the 498 | name of the database table is set and the adapter and the prototype are passed 499 | to the parent constructor. 500 | 501 | - Within the `fetchAlbumList()` method, a `Select` object is created based on 502 | `Zend\Db\Sql`. The data of all albums is fetched from the database and 503 | placed in an array collection with the id of the album as the key. 504 | 505 | - Within the `fetchAlbumById()` method, a `Select` object is created as well. 506 | The selection is limited to the album with the id that was passed to this 507 | method. This method just returns the fetched album. 508 | 509 | - Within the `insertAlbum()` method, an `Insert` object based on `Zend\Db\Sql` 510 | is created. The data of the album is extracted and passed to the `Insert` 511 | instance, and the insertion is executed. If a new row was created, the method 512 | returns `true`, otherwise it returns `false`. 513 | 514 | - Within the `updateAlbum()` method, an `Update` object based on `Zend\Db\Sql` 515 | is created. The data of the album is extracted and passed to the `Update` 516 | instance. Updates are limited to the album passed to the method. When the 517 | update is executed, method returns `true` if an update occurred, and otherwise 518 | returns `false`. 519 | 520 | - Within the `deleteAlbum()` method, a `Delete` object based on `Zend\Db\Sql` is 521 | created. Deletion is limited to the album passed to the method. When the 522 | deletion is executed, the method will return `true` if any rows were deleted, 523 | and otherwise returns `false`. 524 | 525 | Please note that all of these methods either return an `AlbumEntity` or an array 526 | collection of `AlbumEntity` instances, and, if any parameters are accepted, they 527 | typically only accept an `AlbumEntity` instance (with the exception of 528 | `fetchAlbumById()`). There is no need to pass arrays to the command methods or 529 | to handle arrays returned passed from the query methods. 530 | 531 | To get our `AlbumTableGateway` configured properly, we will also need a factory 532 | in the same path. The `AlbumTableGatewayFactory` requests the instance of the 533 | database adapter via the service container (zend-servicemanager in our case), 534 | and then creates a [hydrating](http://zendframework.github.io/zend-hydrator/quick-start/#usage) 535 | result set prototype using `Zend\Hydrator\ArraySerializable` and an 536 | `AlbumEntity` instance. Both the adapter and the prototype are injected into the 537 | constructor of the `AlbumTableGateway`. 538 | 539 | ```php 540 | get(AdapterInterface::class), 564 | $resultSetPrototype 565 | ); 566 | } 567 | } 568 | ``` 569 | 570 | Please note that [zend-hydrator](https://github.com/zendframework/zend-hydrator) 571 | is used to provide de/serialization between `AlbumEntity` instances and the 572 | array data read from the database. The concrete `ArraySerializable` hydrator 573 | uses the methods `exchangeArray()` and `getArrayCopy()` defined in 574 | `Zend\Stdlib\ArraySerializableInterface` and implemented in the `AlbumEntity` . 575 | 576 | ## Create an album repository 577 | 578 | When creating our domain model, we need something to mediate between the domain 579 | objects — our entities — and the storage layer. This is generally 580 | achieved by a [repository](http://martinfowler.com/eaaCatalog/repository.html). 581 | 582 | A repository accepts and returns domain objects, and decides whether or not 583 | storage operations are necessary. Often, they will cache results in order to 584 | reduce overhead on subsequent requests to the same methods, though this is not a 585 | strict requirement. 586 | 587 | We'll now create a repository for the album. The repository will be used within 588 | our middleware actions, and consume an `AlbumStorageInterface` implementation as 589 | developed in the previous section. This will allow us to switch from a database 590 | to a web service, or to use a different implementation than the table gateway, 591 | without needing to change any application code. 592 | 593 | As with storage, we'll start by creating an interface. 594 | Create the directory `src/Album/Model/Repository/` and place the file 595 | `AlbumRepositoryInterface.php` it. This interface is similar to the 596 | `AlbumStorageInterface`, but combines insert and update operations into a single 597 | "save" method. 598 | 599 | ```php 600 | albumStorage = $albumStorage; 679 | } 680 | 681 | /** 682 | * {@inheritDoc} 683 | */ 684 | public function fetchAllAlbums() 685 | { 686 | return $this->albumStorage->fetchAlbumList(); 687 | } 688 | 689 | /** 690 | * {@inheritDoc} 691 | * Fetch a single album 692 | */ 693 | public function fetchSingleAlbum($id) 694 | { 695 | return $this->albumStorage->fetchAlbumById($id); 696 | } 697 | 698 | /** 699 | * {@inheritDoc} 700 | */ 701 | public function saveAlbum(AlbumEntity $album) 702 | { 703 | if (! $album->getId()) { 704 | return $this->albumStorage->insertAlbum($album); 705 | } 706 | 707 | return $this->albumStorage->updateAlbum($album); 708 | } 709 | 710 | /** 711 | * {@inheritDoc} 712 | */ 713 | public function deleteAlbum(AlbumEntity $album) 714 | { 715 | return $this->albumStorage->deleteAlbum($album); 716 | } 717 | } 718 | ``` 719 | 720 | Most methods of this class proxy directly to the appropriate methods of the 721 | storage; only the `saveAlbum()` method does any extra work (to determine whether 722 | an insert or update operation is warranted). 723 | 724 | The `AlbumRepository` needs a factory. Create the file 725 | `AlbumRepositoryFactory.php` within the same directory; in this factory, we'll 726 | request the album storage from the service container, and pass it to the 727 | constructor of the repository. 728 | 729 | ```php 730 | get(AlbumStorageInterface::class) 746 | ); 747 | } 748 | } 749 | ``` 750 | 751 | ## Update the album configuration 752 | 753 | Now that we have storage and our repository sorted, we need to add dependency 754 | configuration to the application. Edit the file 755 | `config/autoload/album.global.php` and add the following configuration to the 756 | `dependencies` section. 757 | 758 | ```php 759 | [ 762 | 'factories' => [ 763 | /* ... */ 764 | 765 | Album\Model\Repository\AlbumRepositoryInterface::class => 766 | Album\Model\Repository\AlbumRepositoryFactory::class, 767 | 768 | Album\Model\Storage\AlbumStorageInterface::class => 769 | Album\Db\AlbumTableGatewayFactory::class, 770 | ], 771 | ], 772 | 773 | /* ... */ 774 | ]; 775 | ``` 776 | 777 | For both the repository and the storage we use the interface names as the 778 | identifier and the factories for the instantiation. 779 | 780 | ## Update the album list middleware 781 | 782 | Now that we have our domain models, repository, and storage created, we can 783 | update our middleware to use them. 784 | 785 | Edit the file `src/Album/Action/AlbumListAction.php` and implement the following 786 | changes: 787 | 788 | ```php 789 | template = $template; 819 | $this->albumRepository = $albumRepository; 820 | } 821 | 822 | /** 823 | * @param ServerRequestInterface $request 824 | * @param ResponseInterface $response 825 | * @param callable|null $next 826 | * @return HtmlResponse 827 | */ 828 | public function __invoke( 829 | ServerRequestInterface $request, 830 | ResponseInterface $response, 831 | callable $next = null 832 | ) { 833 | $data = [ 834 | 'albumList' => $this->albumRepository->fetchAllAlbums(), 835 | ]; 836 | 837 | return new HtmlResponse( 838 | $this->template->render('album::list', $data) 839 | ); 840 | } 841 | } 842 | ``` 843 | 844 | The changes in the above include: 845 | 846 | - Adding another private property, `$albumRepository`, to hold an 847 | `AlbumRepositoryInterface` instance. 848 | 849 | - Changing the constructor to add a second parameter, `$albumRepository`, 850 | accepting an `AlbumRepositoryInterface` instance and assigning it to the 851 | `$albumRepository` property. 852 | 853 | - Filling the `$data` array within the `invoke()` method with a list of 854 | albums fetched from the repository. 855 | 856 | Because we've added a new constructor argument, we will need to 857 | update the `AlbumListFactory`: 858 | 859 | ```php 860 | get(TemplateRendererInterface::class), 878 | $container->get(AlbumRepositoryInterface::class) 879 | ); 880 | } 881 | } 882 | ``` 883 | 884 | ## Update the album list template 885 | 886 | Finally, now that our middleware is passing albums to the template, we need to 887 | update the template to display them. 888 | 889 | The list is presented within a table styled by 890 | [Bootstrap](http://getbootstrap.com). We loop through all albums and echo the 891 | id, artist, and title by accessing the getter methods of the `AlbumEntity`. 892 | 893 | ```php 894 | headTitle('Albums'); 898 | ?> 899 | 900 |
901 |

Album list

902 |
903 | 904 | 905 | 906 | 907 | 908 | 909 | 910 | 911 | 912 | 913 | 914 | albumList as $albumEntity) : ?> 915 | 916 | 917 | 918 | 919 | 920 | 921 | 922 |
IdArtistTitle
getId(); ?>getArtist(); ?>getTitle(); ?>
923 | ``` 924 | 925 | Now you can browse to 926 | [http://localhost:8080/album](http://localhost:8080/album) to see if the 927 | album list is shown as expected. 928 | 929 | ![Screenshot of album list with data](images/screen-album-list-with-data.png) 930 | 931 | ## Update tests for album list middleware 932 | 933 | Let's test that everything works as expected. 934 | 935 | Edit `test/AlbumTest/Action/AlbumListActionTest` to add the injection of 936 | the `AlbumRepositoryInterface` instance, and to mock its call to 937 | `fetchAllAlbums()`. 938 | 939 | ```php 940 | prophesize( 955 | AlbumRepositoryInterface::class 956 | ); 957 | $albumRepository->fetchAllAlbums()->shouldBeCalled()->willReturn([ 958 | 'album1', 959 | 'album2' 960 | ]); 961 | 962 | $renderer = $this->prophesize(TemplateRendererInterface::class); 963 | $renderer->render( 964 | 'album::list', 965 | ['albumList' => ['album1', 'album2']] 966 | )->shouldBeCalled()->willReturn('BODY'); 967 | 968 | $action = new AlbumListAction( 969 | $renderer->reveal(), 970 | $albumRepository->reveal() 971 | ); 972 | 973 | $response = $action( 974 | $this->request->reveal(), 975 | $this->response->reveal(), 976 | $this->next 977 | ); 978 | 979 | $this->assertInstanceOf(HtmlResponse::class, $response); 980 | 981 | $this->assertEquals('BODY', $response->getBody()); 982 | } 983 | } 984 | ``` 985 | 986 | The factory test case also needs to test the injection of the `AlbumRepository`: 987 | 988 | ```php 989 | container 1006 | ->get(TemplateRendererInterface::class) 1007 | ->willReturn( 1008 | $this->prophesize(TemplateRendererInterface::class)->reveal() 1009 | ); 1010 | 1011 | $this->container 1012 | ->get(AlbumRepositoryInterface::class) 1013 | ->willReturn( 1014 | $this->prophesize(AlbumRepositoryInterface::class)->reveal() 1015 | ); 1016 | 1017 | $action = $factory($this->container->reveal()); 1018 | 1019 | $this->assertTrue($action instanceof AlbumListAction); 1020 | } 1021 | } 1022 | ``` 1023 | 1024 | Now run the tests from your project root: 1025 | 1026 | ```bash 1027 | $ phpunit 1028 | ``` 1029 | 1030 | ## Compare with example repository branch `part3` 1031 | 1032 | You can easily compare your code with the example repository when looking 1033 | at the branch `part3`. If you want you can even clone it and have a deeper 1034 | look. 1035 | 1036 | [https://github.com/RalfEggert/zend-expressive-tutorial/tree/part3](https://github.com/RalfEggert/zend-expressive-tutorial/tree/part3) 1037 | -------------------------------------------------------------------------------- /doc/book/part4.md: -------------------------------------------------------------------------------- 1 | # Part 4: Forms and input filter 2 | 3 | In this part of the tutorial we will create an input filter and a form 4 | to allow the user of the album application to add new albums. We will also 5 | need to create some new middleware actions for the form display and 6 | handling. 7 | 8 | ## Add more Zend Framework components 9 | 10 | To make sure that the needed Zend Framework components are installed, you 11 | need to run the Composer to require the 12 | [`Zend\Form`](https://github.com/zendframework/zend-form) and the 13 | [`Zend\InputFilter`](https://github.com/zendframework/zend-inputfilter) 14 | components of the Zend Framework. 15 | 16 | ``` 17 | $ composer require zendframework/zend-inputfilter zendframework/zend-form 18 | ``` 19 | 20 | Please note: since `Zend\Form` currently requires `Zend\InputFilter` we 21 | do not really need to add both. `Zend\Form` would be enough. 22 | 23 | When you run this installation via Composer the `Zend\ComponentInstaller` 24 | steps in here now and asks you if you want to inject the 25 | `Zend\InputFilter\ConfigProvider` into your config file. You should select 26 | it with the choice of `1` and also remember your decision with `y`. It 27 | should look like this: 28 | 29 | ![Install Zend\InputFilter and Zend\Form components](images/install-zend-inputfilter-form.png) 30 | 31 | Please note that your `/config/config.php` should be updated as well by 32 | adding the `Zend\InputFilter\ConfigProvider` and 33 | `Zend\Form\ConfigProvider`: 34 | 35 | ```php 36 | 37 | use Zend\Expressive\ConfigManager\ConfigManager; 38 | use Zend\Expressive\ConfigManager\PhpFileProvider; 39 | 40 | $configManager = new ConfigManager([ 41 | \Zend\Form\ConfigProvider::class, 42 | \Zend\InputFilter\ConfigProvider::class, 43 | \Zend\Hydrator\ConfigProvider::class, 44 | \Zend\Db\ConfigProvider::class, 45 | Zend\Filter\ConfigProvider::class, 46 | Zend\I18n\ConfigProvider::class, 47 | Zend\Router\ConfigProvider::class, 48 | Zend\Validator\ConfigProvider::class, 49 | new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), 50 | ]); 51 | 52 | return new ArrayObject($configManager->getMergedConfig()); 53 | ``` 54 | 55 | ## Create the album input filter 56 | 57 | First, we need to create the album input filter. The `Zend\InputFilter` 58 | component can be used to filter and validate generic sets of input data. 59 | This input filter can work together with the form we will create in the 60 | next step. 61 | 62 | Please create a new path `/src/Album/Model/InputFilter/` and place the new 63 | `AlbumInputFilter.php` file in there. The `AlbumInputFilter` defines two input 64 | elements, one for the artist and one for the title. Both input elements are 65 | mandatory and get a set of filters and validators defined. The id does not 66 | need an input element. 67 | 68 | ```php 69 | add([ 87 | 'name' => 'artist', 88 | 'required' => true, 89 | 'filters' => [ 90 | ['name' => 'StripTags'], 91 | ['name' => 'StringTrim'], 92 | ], 93 | 'validators' => [ 94 | [ 95 | 'name' => 'StringLength', 96 | 'options' => [ 97 | 'min' => 1, 98 | 'max' => 100, 99 | ], 100 | ], 101 | ], 102 | ]); 103 | 104 | $this->add([ 105 | 'name' => 'title', 106 | 'required' => true, 107 | 'filters' => [ 108 | ['name' => 'StripTags'], 109 | ['name' => 'StringTrim'], 110 | ], 111 | 'validators' => [ 112 | [ 113 | 'name' => 'StringLength', 114 | 'options' => [ 115 | 'min' => 1, 116 | 'max' => 100, 117 | ], 118 | ], 119 | ], 120 | ]); 121 | } 122 | } 123 | ``` 124 | 125 | Please note that the adding of the input elements is done in the 126 | `init()` method. This method is automatically called when the input filter 127 | is instantiated through the input filter manager. The input filter manager 128 | is a specialized service-manager just for input filter classes. We won't 129 | use the input filter manager in this tutorial. By implementing the `init()` 130 | method it will be much easier to setup the input filter manager in your 131 | project at a later time. 132 | 133 | Please also note that we have filtering and validation now within the 134 | private setter-methods of the `AlbumEntity` and the `AlbumInputFilter`. 135 | This might look redundant, but there are good reasons for this. 136 | 137 | * The `AlbumEntity` always makes sure that no invalid data is passed by 138 | throwing the exceptions. You cannot set a title with more than 100 chars. 139 | 140 | * The `AlbumInputFilter` also makes sure that no invalid data is passed by 141 | a form. While the exceptions within the `AlbumEntity` will be thrown one 142 | by one, the `AlbumInputFilter` always checks all input data and generates 143 | the error messages. 144 | 145 | * This two-level filtering and validation is a common practice. You could 146 | compare it with JavaScript validation in the front end and PHP validation 147 | in the backend. 148 | 149 | Of course the `AlbumInputFilter` will need a factory as will. So please 150 | create another `AlbumInputFilterFactory.php` file in the same path. The 151 | factory is just instantiating the `AlbumInputFilter` and running the 152 | `init()` method. If you need to add further configuration like some valid 153 | options for another input element you can inject that after instantiation 154 | and before the call of the `init()` method. 155 | 156 | ```php 157 | init(); 178 | 179 | return $inputFilter; 180 | } 181 | } 182 | ``` 183 | 184 | ## Create the album form 185 | 186 | Next we will need to create a form for the album data. The `Zend\Form` 187 | component can be used to structure and display forms. It can work 188 | together with the album input filter we just created. 189 | 190 | Please create another new path `/src/Album/Form/` and place a new file 191 | `AlbumDataForm.php` in there. The `AlbumDataForm` extends the class 192 | `Zend\Form\Form` and defines two form elements (one for the artist and one 193 | for the title) and a submit button. The form elements are setup as text 194 | inputs and named by a label. All elements get some CSS classes defined to 195 | be used by Bootstrap again. 196 | 197 | ```php 198 | setName('album_form'); 216 | $this->setAttribute('class', 'form-horizontal'); 217 | 218 | $this->add( 219 | [ 220 | 'name' => 'artist', 221 | 'type' => 'Text', 222 | 'attributes' => [ 223 | 'class' => 'form-control', 224 | ], 225 | 'options' => [ 226 | 'label' => 'Artist', 227 | 'label_attributes' => [ 228 | 'class' => 'col-sm-2 control-label', 229 | ], 230 | ], 231 | ] 232 | ); 233 | 234 | $this->add( 235 | [ 236 | 'name' => 'title', 237 | 'type' => 'Text', 238 | 'attributes' => [ 239 | 'class' => 'form-control', 240 | ], 241 | 'options' => [ 242 | 'label' => 'Title', 243 | 'label_attributes' => [ 244 | 'class' => 'col-sm-2 control-label', 245 | ], 246 | ], 247 | ] 248 | ); 249 | 250 | $this->add( 251 | [ 252 | 'name' => 'save_album', 253 | 'type' => 'Submit', 254 | 'attributes' => [ 255 | 'class' => 'btn btn-primary', 256 | 'value' => 'Save Album', 257 | 'id' => 'save_album', 258 | ], 259 | ] 260 | ); 261 | } 262 | } 263 | ``` 264 | 265 | Please note that the adding of the form elements is also done in the 266 | `init()` method. This method is automatically called when the form is 267 | instantiated through the form element manager. The form element manager 268 | is a specialized service-manager just for form elements and forms. We won't 269 | use the form element manager in this tutorial. By implementing the `init()` 270 | method it will be much easier to setup the form element manager in your 271 | project at a later time. 272 | 273 | The `AlbumDataForm` also needs a factory which is created in the 274 | `AlbumDataFormFactory.php` file in the same path. This factory instantiates 275 | the `AlbumDataForm` form and injects an instance of the 276 | `Zend\Hydrator\ArraySerializable` and the album input filter we just 277 | created. 278 | 279 | ```php 280 | get(AlbumInputFilter::class); 304 | 305 | $form = new AlbumDataForm(); 306 | $form->setHydrator($hydrator); 307 | $form->setInputFilter($inputFilter); 308 | $form->init(); 309 | 310 | return $form; 311 | } 312 | } 313 | ``` 314 | 315 | Please note that the injection of the `Zend\Hydrator\ArraySerializable` is 316 | done for a special reason. We can now bind an `AlbumEntity` instance to 317 | the form and the hydrator helps to extract the data from the entity and 318 | fill the form elements with these values. We could also pass array data to 319 | the form and get the `AlbumEntity` populated with this data after a 320 | successful form validation. 321 | 322 | ## Update album configuration 323 | 324 | Next, we need to update the album configuration in the 325 | `/config/autload/album.global.php` file. 326 | 327 | ```php 328 | [ 331 | 'factories' => [ 332 | /* ... */ 333 | 334 | Album\Action\AlbumCreateFormAction::class => 335 | Album\Action\AlbumCreateFormFactory::class, 336 | Album\Action\AlbumCreateHandleAction::class => 337 | Album\Action\AlbumCreateHandleFactory::class, 338 | 339 | Album\Form\AlbumDataForm::class => 340 | Album\Form\AlbumDataFormFactory::class, 341 | 342 | Album\Model\InputFilter\AlbumInputFilter::class => 343 | Album\Model\InputFilter\AlbumInputFilterFactory::class, 344 | 345 | /* ... */ 346 | ], 347 | ], 348 | 349 | 'routes' => [ 350 | /* ... */ 351 | 352 | [ 353 | 'name' => 'album-create', 354 | 'path' => '/album/create', 355 | 'middleware' => Album\Action\AlbumCreateFormAction::class, 356 | 'allowed_methods' => ['GET'], 357 | ], 358 | [ 359 | 'name' => 'album-create-handle', 360 | 'path' => '/album/create/handle', 361 | 'middleware' => [ 362 | Album\Action\AlbumCreateHandleAction::class, 363 | Album\Action\AlbumCreateFormAction::class, 364 | ], 365 | 'allowed_methods' => ['POST'], 366 | ], 367 | ], 368 | 369 | /* ... */ 370 | ]; 371 | ``` 372 | 373 | * We have added the DI container configuration for both the album form and 374 | the album input filter in the `dependencies` section. 375 | 376 | * In the `routes` section two new routes were added. 377 | 378 | * The first route is called `album-create` and should process the 379 | `Album\Action\AlbumCreateFormAction` for GET requests. 380 | * The second route is called `album-create-handle` and should process the 381 | `Album\Action\AlbumCreateHandleAction` for POST requests. It also adds 382 | the `Album\Action\AlbumCreateFormAction` to the pipeline as the next 383 | middleware. 384 | 385 | * These two new middleware actions were also added to the `dependencies` 386 | section to inform the DI container of its existence. Both need a factory 387 | to get instantiated. 388 | 389 | ## Create album form create action 390 | 391 | Please create the `AlbumCreateFormAction.php` file in the existing 392 | `/src/Album/Action/` path. The `AlbumCreateFormAction` is used to show the 393 | album form for creating new albums. It won't handle the form processing, 394 | it only passes the form to the template for rendering. And it sets a 395 | message depending on the current form validation state. 396 | 397 | ```php 398 | template = $template; 435 | $this->albumForm = $albumForm; 436 | } 437 | 438 | /** 439 | * @param ServerRequestInterface $request 440 | * @param ResponseInterface $response 441 | * @param callable|null $next 442 | * 443 | * @return HtmlResponse 444 | */ 445 | public function __invoke( 446 | ServerRequestInterface $request, ResponseInterface $response, 447 | callable $next = null 448 | ) { 449 | if ($this->albumForm->getMessages()) { 450 | $message = 'Please check your input!'; 451 | } else { 452 | $message = 'Please enter the new album!'; 453 | } 454 | 455 | $data = [ 456 | 'albumForm' => $this->albumForm, 457 | 'message' => $message, 458 | ]; 459 | 460 | return new HtmlResponse( 461 | $this->template->render('album::create', $data) 462 | ); 463 | } 464 | } 465 | ``` 466 | 467 | * The `AlbumCreateFormAction` has two dependencies. It needs the template 468 | renderer and an instance of the `AlbumDataForm` which can both be 469 | injected to the constructor during instantiation. 470 | 471 | * Within the `__invoke()` method it first checks if the form was validated 472 | with errors to set a different message. This is needed because the 473 | `AlbumCreateFormAction` middleware will also be processed after the 474 | `Album\Action\AlbumCreateHandleAction` when the form validation failed 475 | (see the configuration for the route `album-create-handle` above). 476 | 477 | * After setting a message both the form and the message are passed to the 478 | template renderer which renders the `album::create` template and passes 479 | the generated HTML to the `HtmlResponse`. 480 | 481 | Now the `AlbumCreateFormAction` needs a factory to inject both the 482 | template renderer and the form instance. Please create the 483 | `AlbumCreateFormFactory.php` file to do the job. Both dependencies are 484 | requested from the DI container. 485 | 486 | ```php 487 | get(TemplateRendererInterface::class); 509 | $albumForm = $container->get(AlbumDataForm::class); 510 | 511 | return new AlbumCreateFormAction( 512 | $template, $albumForm 513 | ); 514 | } 515 | } 516 | ``` 517 | 518 | ## Create album form handling action 519 | 520 | Now create the `AlbumCreateHandleAction.php` file within the same path. 521 | This middleware action is used for the form handling of the album form. 522 | It can only be accessed when a POST request is send to the path of the 523 | `album-create-handle` route (see the configuration for the route above). 524 | 525 | ```php 526 | router = $router; 573 | $this->albumRepository = $albumRepository; 574 | $this->albumForm = $albumForm; 575 | } 576 | 577 | /** 578 | * @param ServerRequestInterface $request 579 | * @param ResponseInterface $response 580 | * @param callable|null $next 581 | * 582 | * @return HtmlResponse 583 | */ 584 | public function __invoke( 585 | ServerRequestInterface $request, ResponseInterface $response, 586 | callable $next = null 587 | ) { 588 | $postData = $request->getParsedBody(); 589 | 590 | $this->albumForm->setData($postData); 591 | 592 | if ($this->albumForm->isValid()) { 593 | $album = new AlbumEntity(); 594 | $album->exchangeArray($postData); 595 | 596 | if ($this->albumRepository->saveAlbum($album)) { 597 | return new RedirectResponse( 598 | $this->router->generateUri('album') 599 | ); 600 | } 601 | } 602 | 603 | return $next($request, $response); 604 | } 605 | } 606 | ``` 607 | 608 | * The class `AlbumCreateHandleAction` has three dependencies: 609 | 610 | * It needs the instance of the router to generate an URL for a route to 611 | redirect to. 612 | * It needs the instance of the album repository to be able to save the 613 | new album. 614 | * It needs the instance of the album data form for the form handling 615 | and validation. 616 | 617 | * All dependencies can be injected via the constructor. 618 | 619 | * In the `__invoke()` method the form handling is processed. 620 | 621 | * First the post data is accessed from the request. 622 | * Then the post data is passed to the form. 623 | * Then the form validation is started. 624 | * If the form validation was successful... 625 | * A new `AlbumEntity` is created 626 | * The data is passed to its `exchangeArray()` method. 627 | * This new entity is saved with the repository. 628 | * A `RedirectResponse` is created to redirect to the album list. 629 | 630 | * If the form validation failed... 631 | * The next middleware is processed. From the route configuration you 632 | know that the `AlbumCreateFormAction` is the next middleware. This 633 | will show the create form now but with all the validation error 634 | messages. 635 | 636 | Of course, the `AlbumCreateHandleAction` also needs a factory. Please 637 | place it within the same path. The factory requests the three dependencies 638 | from the DI container and passes them to the constructor of the class. 639 | 640 | ```php 641 | get(RouterInterface::class); 665 | $albumRepository = $container->get(AlbumRepositoryInterface::class); 666 | $albumForm = $container->get(AlbumDataForm::class); 667 | 668 | return new AlbumCreateHandleAction( 669 | $router, $albumRepository, $albumForm 670 | ); 671 | } 672 | } 673 | ``` 674 | 675 | ## Create album creation template 676 | 677 | Next, you need to create the `create.phtml` file in the existing 678 | `/templates/album/` path. This template should render the album data form. 679 | 680 | ```php 681 | albumForm; 686 | $form->setAttribute('action', $this->url('album-create-handle')); 687 | 688 | $this->headTitle('Create new album'); 689 | ?> 690 | 691 |
692 |

Create new album

693 |
694 | 695 |
696 | message; ?> 697 |
698 | 699 |
700 | form()->openTag($form); ?> 701 |
702 | formLabel($form->get('artist')); ?> 703 |
704 | formElement($form->get('artist')); ?> 705 | formElementErrors($form->get('artist')); ?> 706 |
707 |
708 |
709 | formLabel($form->get('title')); ?> 710 |
711 | formElement($form->get('title')); ?> 712 | formElementErrors($form->get('title')); ?> 713 |
714 |
715 |
716 |
717 | formElement($form->get('save_album')); ?> 718 |
719 |
720 | form()->closeTag(); ?> 721 |
722 | 723 |

724 | 725 | Back to album list 726 | 727 |

728 | ``` 729 | 730 | * It sets the form action to the url for the `album-create-handle` route 731 | which is generated with the `url` view helper. 732 | 733 | * It displays the heading and the current message. 734 | 735 | * It renders the form by using the `form`, the `formLabel`, the 736 | `formElement` and the `formElementErrors` view helpers for the two form 737 | elements and the submit button. 738 | 739 | * At the bottom it displays a link back to the album list. 740 | 741 | ## Add link to the album list page 742 | 743 | Finally, you only need to add a link button to the album list page. Please open 744 | the `list.phtml` file in the `/templates/album/` path. Add the link at the 745 | bottom of the page by using the `url` view helper with the `album-create` 746 | route. 747 | 748 | ```php 749 |

750 | 751 | Create new album 752 | 753 |

754 | ``` 755 | 756 | Now you can browse to 757 | [http://localhost:8080/album/create](http://localhost:8080/album/create) 758 | to see if the album form is shown as expected. Please try to enter a new 759 | album with no data and with valid data and see what happens. If the 760 | creation of a new album is successful it will be displayed in the album 761 | list. 762 | 763 | ![Screenshot of album create form](images/screen-album-create-form.png) 764 | 765 | ## Compare with example repository branch `part4` 766 | 767 | You can easily compare your code with the example repository when looking 768 | at the branch `part4`. If you want you can even clone it and have a deeper 769 | look. 770 | 771 | [https://github.com/RalfEggert/zend-expressive-tutorial/tree/part4](https://github.com/RalfEggert/zend-expressive-tutorial/tree/part4) 772 | -------------------------------------------------------------------------------- /doc/book/part5.md: -------------------------------------------------------------------------------- 1 | # Part 5: Updating and deleting albums 2 | 3 | In this part of the tutorial we will handle the updating and deleting of 4 | existing albums. Similar to the album creation action we will create all 5 | actions and factories needed. For the update we can reuse the album data 6 | form, for deletion we will create a new delete form. 7 | 8 | ## Update album configuration 9 | 10 | First, we will update the album configuration in the 11 | `/config/autoload/album.global.php` file to add new middleware actions, 12 | a form and some routes. 13 | 14 | * In the `dependencies` section four new middleware with its factories are 15 | added. These will be created in the next steps. 16 | 17 | * The `AlbumUpdateFormAction` will show the update form for an album. 18 | * The `AlbumUpdateHandleAction` will handle the update form processing. 19 | * The `AlbumDeleteFormAction` will show the delete form for an album. 20 | * The `AlbumDeleteHandleAction` will handle the update form processing. 21 | 22 | * Additionally, a new delete album form is also registered for the DI 23 | Container. The form will be created as well in the next steps. 24 | 25 | * The the `routes` section four new routes will be added for the four new 26 | middleware actions. Some are only processed for GET requests, some only 27 | for POST requests. 28 | 29 | 30 | ```php 31 | [ 34 | 'factories' => [ 35 | /* ... */ 36 | 37 | Album\Action\AlbumUpdateFormAction::class => 38 | Album\Action\AlbumUpdateFormFactory::class, 39 | Album\Action\AlbumUpdateHandleAction::class => 40 | Album\Action\AlbumUpdateHandleFactory::class, 41 | Album\Action\AlbumDeleteFormAction::class => 42 | Album\Action\AlbumDeleteFormFactory::class, 43 | Album\Action\AlbumDeleteHandleAction::class => 44 | Album\Action\AlbumDeleteHandleFactory::class, 45 | 46 | /* ... */ 47 | 48 | Album\Form\AlbumDeleteForm::class => 49 | Album\Form\AlbumDeleteFormFactory::class, 50 | 51 | /* ... */ 52 | ], 53 | ], 54 | 55 | 'routes' => [ 56 | /* ... */ 57 | 58 | [ 59 | 'name' => 'album-update', 60 | 'path' => '/album/update/:id', 61 | 'middleware' => Album\Action\AlbumUpdateFormAction::class, 62 | 'allowed_methods' => ['GET'], 63 | 'options' => [ 64 | 'constraints' => [ 65 | 'id' => '[1-9][0-9]*', 66 | ], 67 | ], 68 | ], 69 | [ 70 | 'name' => 'album-update-handle', 71 | 'path' => '/album/update/:id/handle', 72 | 'middleware' => [ 73 | Album\Action\AlbumUpdateHandleAction::class, 74 | Album\Action\AlbumUpdateFormAction::class, 75 | ], 76 | 'allowed_methods' => ['POST'], 77 | 'options' => [ 78 | 'constraints' => [ 79 | 'id' => '[1-9][0-9]*', 80 | ], 81 | ], 82 | ], 83 | [ 84 | 'name' => 'album-delete', 85 | 'path' => '/album/delete/:id', 86 | 'middleware' => Album\Action\AlbumDeleteFormAction::class, 87 | 'allowed_methods' => ['GET'], 88 | 'options' => [ 89 | 'constraints' => [ 90 | 'id' => '[1-9][0-9]*', 91 | ], 92 | ], 93 | ], 94 | [ 95 | 'name' => 'album-delete-handle', 96 | 'path' => '/album/delete/:id/handle', 97 | 'middleware' => Album\Action\AlbumDeleteHandleAction::class, 98 | 'allowed_methods' => ['POST'], 99 | 'options' => [ 100 | 'constraints' => [ 101 | 'id' => '[1-9][0-9]*', 102 | ], 103 | ], 104 | ], 105 | ], 106 | 107 | /* ... */ 108 | ]; 109 | ``` 110 | 111 | ## Add links to the album list page 112 | 113 | Next, you need to add a link to the update and the delete page for each 114 | album in the album list. Please open the `/templates/album/list.phtml` and 115 | update the `foreach()` loop. You can generate the URLs with the `url` view 116 | helper and display them in the table at the end of each row. 117 | 118 | ```php 119 | albumList as $albumEntity) : ?> 120 | $albumEntity->getId()]; 122 | $updateUrl = $this->url('album-update', $urlParams); 123 | $deleteUrl = $this->url('album-delete', $urlParams); 124 | ?> 125 | 126 | getId(); ?> 127 | getArtist(); ?> 128 | getTitle(); ?> 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | ``` 140 | 141 | Now you can browse to [http://localhost:8080/album](http://localhost:8080/album) 142 | to see if the links for update and delete are shown correctly. Don't click 143 | on the yet since we have no update and delete action yet 144 | 145 | ![Screenshot of album list with links](images/screen-album-list-links.png) 146 | 147 | ## Add update action to show form 148 | 149 | Next, you need to create the `AlbumUpdateFormAction.php` file in the 150 | existing `/src/Album/Action/` path. Please note the following: 151 | 152 | * The `AlbumUpdateFormAction` has three dependencies to the template 153 | renderer, the album repository and the album form. All of these 154 | dependencies can be injected with the constructor. 155 | 156 | * The `__invoke()` method is run when the middleware is processed. 157 | 158 | * First the `id` is taken from the routing to read the current 159 | `AlbumEntity` to update. 160 | 161 | * If the form validation was started and failed, then the form has some 162 | messages set. In that case an appropriate message is set for the form. 163 | 164 | * If the form validation was not run, then a different message is set for 165 | the form. Additionally, the `AlbumEntity` instance is bound to the 166 | form. When this is done the form uses the injected hydrator to extract 167 | the data from the entity and passes it to the form elements to set 168 | their values. 169 | 170 | * Next, the `$data` array is built with the form, the entity and the 171 | message. 172 | 173 | * Finally, the update template is rendered and a `HtmlResponse` is 174 | passed back. 175 | 176 | ```php 177 | template = $template; 222 | $this->albumRepository = $albumRepository; 223 | $this->albumForm = $albumForm; 224 | } 225 | 226 | /** 227 | * @param ServerRequestInterface $request 228 | * @param ResponseInterface $response 229 | * @param callable|null $next 230 | * 231 | * @return HtmlResponse 232 | */ 233 | public function __invoke( 234 | ServerRequestInterface $request, 235 | ResponseInterface $response, 236 | callable $next = null 237 | ) { 238 | $id = $request->getAttribute('id'); 239 | 240 | $album = $this->albumRepository->fetchSingleAlbum($id); 241 | 242 | if ($this->albumForm->getMessages()) { 243 | $message = 'Please check your input!'; 244 | } else { 245 | $message = 'Please change the album!'; 246 | 247 | $this->albumForm->bind($album); 248 | } 249 | 250 | $data = [ 251 | 'albumForm' => $this->albumForm, 252 | 'albumEntity' => $album, 253 | 'message' => $message, 254 | ]; 255 | 256 | return new HtmlResponse( 257 | $this->template->render('album::update', $data) 258 | ); 259 | } 260 | } 261 | ``` 262 | 263 | The corresponding factory will be created in the new 264 | `AlbumUpdateFormFactory.php` file. It looks much similar to the 265 | `AlbumCreateFormFactory` and requests the three dependencies from the DI 266 | container to pass them to the constructor of the `AlbumUpdateFormAction`. 267 | 268 | ```php 269 | get(TemplateRendererInterface::class); 292 | $albumRepository = $container->get(AlbumRepositoryInterface::class); 293 | $albumForm = $container->get(AlbumDataForm::class); 294 | 295 | return new AlbumUpdateFormAction( 296 | $template, $albumRepository, $albumForm 297 | ); 298 | } 299 | } 300 | ``` 301 | 302 | ## Add update action for form handling 303 | 304 | Now you have to create the `AlbumUpdateHandleAction.php` file in the 305 | existing `/src/Album/Action/` path to handle the update form processing. 306 | Please note the following: 307 | 308 | * The `AlbumUpdateHandleAction` has three dependencies to the router, the 309 | album repository and the album form. All of these dependencies can be 310 | injected with the constructor. 311 | 312 | * The `__invoke()` method is run when the form is processed. 313 | 314 | * The `id` is read from the request attributes. 315 | 316 | * The POST data is also read from the request. 317 | 318 | * Then the POST data is passed to the form and the form is validated. 319 | 320 | * If the validation was successful... 321 | 322 | * The `AlbumEntity` is fetched from the repository and the POST data 323 | is passed to it. 324 | 325 | * The album is saved and a redirect to the album list is made. 326 | 327 | * If the form validation failed... 328 | 329 | * The next middleware is processed which is the `AlbumUpdateFormAction` 330 | to show the update form. 331 | 332 | ```php 333 | router = $router; 380 | $this->albumRepository = $albumRepository; 381 | $this->albumForm = $albumForm; 382 | } 383 | 384 | /** 385 | * @param ServerRequestInterface $request 386 | * @param ResponseInterface $response 387 | * @param callable|null $next 388 | * 389 | * @return HtmlResponse 390 | */ 391 | public function __invoke( 392 | ServerRequestInterface $request, 393 | ResponseInterface $response, 394 | callable $next = null 395 | ) { 396 | $id = $request->getAttribute('id'); 397 | 398 | $postData = $request->getParsedBody(); 399 | 400 | $this->albumForm->setData($postData); 401 | 402 | if ($this->albumForm->isValid()) { 403 | $postData['id'] = $id; 404 | 405 | $album = $this->albumRepository->fetchSingleAlbum($id); 406 | $album->exchangeArray($postData); 407 | 408 | $this->albumRepository->saveAlbum($album); 409 | 410 | return new RedirectResponse( 411 | $this->router->generateUri('album') 412 | ); 413 | } 414 | 415 | return $next($request, $response); 416 | } 417 | } 418 | ``` 419 | 420 | The needed factory will be created in the new 421 | `AlbumUpdateHandleFactory.php` file. It looks much similar to the 422 | `AlbumCreateHandleFactory` and requests the three needed dependencies from 423 | the DI container to pass them to the constructor of the 424 | `AlbumUpdateHandleAction`. 425 | 426 | ```php 427 | get(RouterInterface::class); 451 | $albumRepository = $container->get(AlbumRepositoryInterface::class); 452 | $albumForm = $container->get(AlbumDataForm::class); 453 | 454 | return new AlbumUpdateHandleAction( 455 | $router, $albumRepository, $albumForm 456 | ); 457 | } 458 | } 459 | ``` 460 | 461 | ## Create update template 462 | 463 | Now you need to create the `update.phtml` file in the `/templates/album/` 464 | path. In this template you need to setup the form with an form action and 465 | display it. Again the form is rendered by using the `form`, the 466 | `formLabel`, the `formElement` and the `formElementErrors` view helpers 467 | for the form elements and the submit button. 468 | 469 | ```php 470 | albumEntity; 476 | 477 | /** @var AlbumDataForm $form */ 478 | $form = $this->albumForm; 479 | $form->setAttribute( 480 | 'action', $this->url('album-update-handle', ['id' => $album->getId()]) 481 | ); 482 | 483 | $this->headTitle('Edit album'); 484 | ?> 485 | 486 |
487 |

Edit album

488 |
489 | 490 |
491 | message; ?> 492 |
493 | 494 |
495 | form()->openTag($form); ?> 496 |
497 | formLabel($form->get('artist')); ?> 498 |
499 | formElement($form->get('artist')); ?> 500 | formElementErrors($form->get('artist')); ?> 501 |
502 |
503 |
504 | formLabel($form->get('title')); ?> 505 |
506 | formElement($form->get('title')); ?> 507 | formElementErrors($form->get('title')); ?> 508 |
509 |
510 |
511 |
512 | formElement($form->get('save_album')); ?> 513 |
514 |
515 | form()->closeTag(); ?> 516 |
517 | 518 |

519 | 520 | Back to album list 521 | 522 |

523 | ``` 524 | 525 | Now you can browse to 526 | [http://localhost:8080/album/update/1](http://localhost:8080/album/update/1) 527 | to see if the update form works correctly. Try to change the album and save 528 | it. 529 | 530 | ![Screenshot of album update](images/screen-album-update.png) 531 | 532 | ## Add delete form 533 | 534 | To delete an album we will create another form with two submit buttons. 535 | Please create the new file `AlbumDeleteForm.php` in the `/src/Album/Form/` 536 | path. This form just adds two submit buttons. One to confirm the deletion 537 | and on to cancel it. 538 | 539 | ```php 540 | setName('album_delete_form'); 558 | $this->setAttribute('class', 'form-horizontal'); 559 | 560 | $this->add( 561 | [ 562 | 'name' => 'delete_album_yes', 563 | 'type' => 'Submit', 564 | 'attributes' => [ 565 | 'class' => 'btn btn-danger', 566 | 'value' => 'Yes', 567 | 'id' => 'delete_album_yes', 568 | ], 569 | ] 570 | ); 571 | 572 | $this->add( 573 | [ 574 | 'name' => 'delete_album_no', 575 | 'type' => 'Submit', 576 | 'attributes' => [ 577 | 'class' => 'btn btn-default', 578 | 'value' => 'No', 579 | 'id' => 'delete_album_no', 580 | ], 581 | ] 582 | ); 583 | } 584 | } 585 | ``` 586 | 587 | The `AlbumDeleteFormFactory` for this form is created in the 588 | `AlbumDeleteFormFactory.php` file and quite simple. It needs no 589 | dependencies to inject and just runs the `init()` method of the form. 590 | 591 | ```php 592 | init(); 614 | 615 | return $form; 616 | } 617 | } 618 | ``` 619 | 620 | ## Add delete action to show form 621 | 622 | Again we will need a middleware action to show the form. This is done 623 | in the `AlbumDeleteFormAction.php` file. This middleware is very similar 624 | to the `AlbumUpdateFormAction` above. The main difference is that it uses 625 | the new delete form and sets a different message. 626 | 627 | ```php 628 | template = $template; 673 | $this->albumRepository = $albumRepository; 674 | $this->albumForm = $albumForm; 675 | } 676 | 677 | /** 678 | * @param ServerRequestInterface $request 679 | * @param ResponseInterface $response 680 | * @param callable|null $next 681 | * 682 | * @return HtmlResponse 683 | */ 684 | public function __invoke( 685 | ServerRequestInterface $request, 686 | ResponseInterface $response, 687 | callable $next = null 688 | ) { 689 | $id = $request->getAttribute('id'); 690 | 691 | $album = $this->albumRepository->fetchSingleAlbum($id); 692 | 693 | $message = 'Do you want to delete this album?'; 694 | 695 | $this->albumForm->bind($album); 696 | 697 | $data = [ 698 | 'albumEntity' => $album, 699 | 'albumForm' => $this->albumForm, 700 | 'message' => $message, 701 | ]; 702 | 703 | return new HtmlResponse( 704 | $this->template->render('album::delete', $data) 705 | ); 706 | } 707 | } 708 | ``` 709 | 710 | The `AlbumDeleteFormFactory` is also quite similar to the 711 | `AlbumUpdateFormFactory` above. The only difference is the injection of 712 | the delete form during instantiation of `AlbumDeleteFormAction`. 713 | 714 | ```php 715 | get(TemplateRendererInterface::class); 738 | $albumRepository = $container->get(AlbumRepositoryInterface::class); 739 | $albumForm = $container->get(AlbumDeleteForm::class); 740 | 741 | return new AlbumDeleteFormAction( 742 | $template, $albumRepository, $albumForm 743 | ); 744 | } 745 | } 746 | ``` 747 | 748 | ## Add delete action for form handling 749 | 750 | For the deletion handling you need to create the `AlbumDeleteFormAction.php` 751 | which works different than the form handling middleware actions for album 752 | creation and updating. In the `__invoke()` method it checks if the 753 | deletion confirm button named `delete_album_yes` was sent to delete the 754 | album. No matter which submit button was sent, a redirect to the album 755 | list is created at the end. 756 | 757 | ```php 758 | router = $router; 797 | $this->albumRepository = $albumRepository; 798 | } 799 | 800 | /** 801 | * @param ServerRequestInterface $request 802 | * @param ResponseInterface $response 803 | * @param callable|null $next 804 | * 805 | * @return HtmlResponse 806 | */ 807 | public function __invoke( 808 | ServerRequestInterface $request, 809 | ResponseInterface $response, 810 | callable $next = null 811 | ) { 812 | $id = $request->getAttribute('id'); 813 | 814 | $album = $this->albumRepository->fetchSingleAlbum($id); 815 | 816 | $postData = $request->getParsedBody(); 817 | 818 | if (isset($postData['delete_album_yes'])) { 819 | $this->albumRepository->deleteAlbum($album); 820 | } 821 | 822 | return new RedirectResponse( 823 | $this->router->generateUri('album') 824 | ); 825 | } 826 | } 827 | ``` 828 | 829 | The `AlbumDeleteHandleFactory` just requests the router and the album 830 | repository and injects them into the instantiation of the 831 | `AlbumDeleteHandleAction`. 832 | 833 | ```php 834 | get(RouterInterface::class); 857 | $albumRepository = $container->get(AlbumRepositoryInterface::class); 858 | 859 | return new AlbumDeleteHandleAction( 860 | $router, $albumRepository 861 | ); 862 | } 863 | } 864 | ``` 865 | 866 | ## Create delete templates 867 | 868 | Finally, the `delete.phtml` template file in the `/templates/album/` path 869 | is needed to setup the form action and to display the delete form. 870 | 871 | ```php 872 | albumEntity; 878 | 879 | /** @var AlbumDataForm $form */ 880 | $form = $this->albumForm; 881 | $form->setAttribute( 882 | 'action', $this->url('album-delete-handle', ['id' => $album->getId()]) 883 | ); 884 | 885 | $this->headTitle('Delete album'); 886 | ?> 887 | 888 |
889 |

Delete album

890 |
891 | 892 | 893 | 894 | 895 | 896 | 897 | 898 | 899 | 900 | 901 | 902 | 903 | 904 | 905 |
IdgetId(); ?>
ArtistgetArtist(); ?>
TitlegetTitle(); ?>
906 | 907 |
908 | message; ?> 909 |
910 | 911 |
912 | form()->openTag($form); ?> 913 |
914 |
915 | formElement($form->get('delete_album_yes')); ?> 916 | formElement($form->get('delete_album_no')); ?> 917 |
918 |
919 | form()->closeTag(); ?> 920 |
921 | 922 |

923 | 924 | Back to album list 925 | 926 |

927 | ``` 928 | 929 | Now you can browse to 930 | [http://localhost:8080/album/delete/1](http://localhost:8080/album/delete/1) 931 | to see if the delete form works correctly. Try to delete the album. 932 | 933 | ![Screenshot of album delete](images/screen-album-delete.png) 934 | 935 | ## Compare with example repository branch `part5` 936 | 937 | You can easily compare your code with the example repository when looking 938 | at the branch `part5`. If you want you can even clone it and have a deeper 939 | look. 940 | 941 | [https://github.com/RalfEggert/zend-expressive-tutorial/tree/part5](https://github.com/RalfEggert/zend-expressive-tutorial/tree/part5) 942 | -------------------------------------------------------------------------------- /doc/book/part6.md: -------------------------------------------------------------------------------- 1 | # Part 6: Refactor application structure 2 | 3 | In this last and very short part we will refactor the application 4 | structure a little bit. We will delete the old home page and ping actions 5 | and remove all unused files. After this the album page should be shown as 6 | the new home page. 7 | 8 | ## Delete unused files 9 | 10 | Please delete the following file paths and all of the files in there: 11 | 12 | * `/src/App/Action/` 13 | * `/templates/app/` 14 | * `/test/AppTest/Action/` 15 | 16 | ## Remove unused configuration 17 | 18 | Please remove the following unused configuration: 19 | 20 | * The `autoload-dev` section from the `composer.json` file. 21 | 22 | * The `App\Action\PingAction` and `App\Action\HomePageAction` from the 23 | `dependencies` section of the `/config/autoload/routes.global.php` file. 24 | 25 | * The two routes from the `routes` section of the 26 | `/config/autoload/routes.global.php` file. 27 | 28 | * The `app` path from the `paths` of the `templates` section of the 29 | `/config/autoload/templates.global.php` file. 30 | 31 | ## Update templates 32 | 33 | In the `/templates/error/404.phtml` file please change the link to the 34 | home page to: 35 | 36 | ```php 37 | url('album') ?>">Album 38 | ``` 39 | 40 | In the `/templates/layout/default.phtml` file please change the link to the 41 | logo to: 42 | 43 | ```php 44 | url('home') ?> 45 | ``` 46 | 47 | In the navbar you can delete the three menu options with the links to the 48 | `Docs`, the `Contribute` and the `Ping Test`. 49 | 50 | ## Finish 51 | 52 | Now you are done with your first PSR-7 middleware. You have created a 53 | lightweight application to handle albums with `Zend\Expressive`. Please 54 | have a closer look at the generated code now and try to understand 55 | everything you have done in the six parts of this tutorial. 56 | 57 | ## Compare with example repository branch `part6` 58 | 59 | You can easily compare your code with the example repository when looking 60 | at the branch `part6`. If you want you can even clone it and have a deeper 61 | look. 62 | 63 | [https://github.com/RalfEggert/zend-expressive-tutorial/tree/part6](https://github.com/RalfEggert/zend-expressive-tutorial/tree/part6) 64 | -------------------------------------------------------------------------------- /doc/bookdown.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Zend\\Expressive tutorial", 3 | "content": [ 4 | {"Part 1: Setup the application": "book/part1.md"}, 5 | {"Part 2: Album list middleware": "book/part2.md"}, 6 | {"Part 3: Model and database": "book/part3.md"}, 7 | {"Part 4: Forms and input filter": "book/part4.md"}, 8 | {"Part 5: Updating and deleting albums": "book/part5.md"}, 9 | {"Part 6: Refactor application structure": "book/part6.md"} 10 | ], 11 | "target": "./html/" 12 | } -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Zend Framework coding standard 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | src 20 | 21 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ./test 9 | 10 | 11 | 12 | 13 | 14 | ./src 15 | 16 | ./src/ExpressiveInstaller/Resources 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | # The following rule tells Apache that if the requested filename 3 | # exists, simply serve it. 4 | RewriteCond %{REQUEST_FILENAME} -s [OR] 5 | RewriteCond %{REQUEST_FILENAME} -l [OR] 6 | RewriteCond %{REQUEST_FILENAME} -d 7 | RewriteRule ^.*$ - [NC,L] 8 | 9 | # The following rewrites all other queries to index.php. The 10 | # condition ensures that if you are using Apache aliases to do 11 | # mass virtual hosting, the base path will be prepended to 12 | # allow proper resolution of the index.php file; it will work 13 | # in non-aliased environments as well, providing a safe, one-size 14 | # fits all solution. 15 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$ 16 | RewriteRule ^(.*) - [E=BASE:%1] 17 | RewriteRule ^(.*)$ %{ENV:BASE}index.php [NC,L] 18 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | get(\Zend\Expressive\Application::class); 18 | $app->run(); 19 | -------------------------------------------------------------------------------- /public/zf-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/public/zf-logo.png -------------------------------------------------------------------------------- /src/App/Action/HomePageAction.php: -------------------------------------------------------------------------------- 1 | router = $router; 24 | $this->template = $template; 25 | } 26 | 27 | public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null) 28 | { 29 | $data = []; 30 | 31 | if ($this->router instanceof Router\AuraRouter) { 32 | $data['routerName'] = 'Aura.Router'; 33 | $data['routerDocs'] = 'http://auraphp.com/packages/2.x/Router.html'; 34 | } elseif ($this->router instanceof Router\FastRouteRouter) { 35 | $data['routerName'] = 'FastRoute'; 36 | $data['routerDocs'] = 'https://github.com/nikic/FastRoute'; 37 | } elseif ($this->router instanceof Router\ZendRouter) { 38 | $data['routerName'] = 'Zend Router'; 39 | $data['routerDocs'] = 'http://framework.zend.com/manual/current/en/modules/zend.mvc.routing.html'; 40 | } 41 | 42 | if ($this->template instanceof PlatesRenderer) { 43 | $data['templateName'] = 'Plates'; 44 | $data['templateDocs'] = 'http://platesphp.com/'; 45 | } elseif ($this->template instanceof TwigRenderer) { 46 | $data['templateName'] = 'Twig'; 47 | $data['templateDocs'] = 'http://twig.sensiolabs.org/documentation'; 48 | } elseif ($this->template instanceof ZendViewRenderer) { 49 | $data['templateName'] = 'Zend View'; 50 | $data['templateDocs'] = 'http://framework.zend.com/manual/current/en/modules/zend.view.quick-start.html'; 51 | } 52 | 53 | if (!$this->template) { 54 | return new JsonResponse([ 55 | 'welcome' => 'Congratulations! You have installed the zend-expressive skeleton application.', 56 | 'docsUrl' => 'zend-expressive.readthedocs.org', 57 | ]); 58 | } 59 | 60 | return new HtmlResponse($this->template->render('app::home-page', $data)); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/App/Action/HomePageFactory.php: -------------------------------------------------------------------------------- 1 | get(RouterInterface::class); 14 | $template = ($container->has(TemplateRendererInterface::class)) 15 | ? $container->get(TemplateRendererInterface::class) 16 | : null; 17 | 18 | return new HomePageAction($router, $template); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/App/Action/PingAction.php: -------------------------------------------------------------------------------- 1 | time()]); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RalfEggert/zend-expressive-tutorial/ae9b3631a0274dc74797815aa41c12bcb3b118ef/templates/.gitkeep -------------------------------------------------------------------------------- /templates/app/home-page.phtml: -------------------------------------------------------------------------------- 1 | headTitle('Home'); ?> 2 | 3 |
4 |

Welcome to zend-expressive

5 |

6 | Congratulations! You have successfully installed the 7 | zend-expressive skeleton application. 8 | This skeleton can serve as a simple starting point for you to begin building your application. 9 |

10 |

11 | Expressive builds on zend-stratigility to provide a minimalist PSR-7 middleware framework for PHP. 12 |

13 |
14 | 15 |
16 |
17 |

18 | 19 | Agile & Lean 20 | 21 |

22 |

23 | Expressive is fast, small and perfect for rapid application development, prototyping and api's. You decide how you 24 | extend it and choose the best packages from major framework or standalone projects. 25 |

26 |
27 | 28 |
29 |

30 | 31 | HTTP Messages 32 | 33 |

34 |

35 | HTTP messages are the foundation of web development. Web browsers and HTTP clients such as cURL create 36 | HTTP request messages that are sent to a web server, which provides an HTTP response message. 37 | Server-side code receives an HTTP request message, and returns an HTTP response message. 38 |

39 |
40 | 41 |
42 |

43 | 44 | Middleware 45 | 46 |

47 |

48 | Middleware is code that exists between the request and response, and which can take the incoming 49 | request, perform actions based on it, and either complete the response or pass delegation on to the 50 | next middleware in the queue. Your application is easily extended with custom middleware created by 51 | yourself or others. 52 |

53 |
54 |
55 | 56 |
57 |
58 |

59 | 60 | Containers 61 | 62 |

63 |

64 | Expressive promotes and advocates the usage of Dependency Injection/Inversion of Control containers 65 | when writing your applications. Expressive supports multiple containers which typehints against 66 | container-interop. 67 |

68 |
69 | 70 |
71 |

72 | 73 | Routers 74 | 75 |

76 |

77 | One fundamental feature of zend-expressive is that it provides mechanisms for implementing dynamic 78 | routing, a feature required in most modern web applications. Expressive ships with multiple adapters. 79 |

80 | routerName)) : ?> 81 |

82 | 83 | Get started with routerName ?>. 84 | 85 |

86 | 87 |
88 | 89 |
90 |

91 | 92 | Templating 93 | 94 |

95 |

96 | By default, no middleware in Expressive is templated. We do not even provide a default templating 97 | engine, as the choice of templating engine is often very specific to the project and/or organization. 98 | However, Expressive does provide abstraction for templating, which allows you to write middleware that 99 | is engine-agnostic. 100 |

101 | templateName)) : ?> 102 |

103 | 104 | Get started with templateName ?>. 105 | 106 |

107 | 108 |
109 |
110 | -------------------------------------------------------------------------------- /templates/error/404.phtml: -------------------------------------------------------------------------------- 1 | headTitle('404 Not Found'); ?> 2 | 3 |

Oops!

4 |

This is awkward.

5 |

We encountered a 404 Not Found error.

6 |

7 | You are looking for something that doesn't exist or may have moved. Check out one of the links on this page 8 | or head back to Home. 9 |

10 | -------------------------------------------------------------------------------- /templates/error/error.phtml: -------------------------------------------------------------------------------- 1 | headTitle(sprintf('%d %s', $this->status, $this->reason)); ?> 2 | 3 |

Oops!

4 |

This is awkward.

5 |

We encountered a escapeHtml(sprintf('%d %s', $this->status, $this->reason)); ?> error.

6 | status == 404) : ?> 7 |

8 | You are looking for something that doesn't exist or may have moved. Check out one of the links on this page 9 | or head back to Home. 10 |

11 | 12 | -------------------------------------------------------------------------------- /templates/layout/default.phtml: -------------------------------------------------------------------------------- 1 | headLink() 3 | ->prependStylesheet('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css') 4 | ->prependStylesheet('https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css'); 5 | 6 | $this->inlineScript() 7 | ->prependFile('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js') 8 | ->prependFile('https://code.jquery.com/jquery-2.1.4.min.js'); 9 | ?> 10 | 11 | 12 | 13 | 14 | 15 | headTitle('zend-expressive')->setSeparator(' - ')->setAutoEscape(false) ?> 16 | 17 | headMeta(); ?> 18 | headLink(); ?> 19 | 26 | 27 | 28 |
29 | 62 |
63 | 64 |
65 |
66 | content ?> 67 |
68 |
69 | 70 |
71 |
72 |
73 |

74 | © 2005 - by Zend Technologies Ltd. All rights reserved. 75 |

76 |
77 |
78 | 79 | inlineScript(); ?> 80 | 81 | 82 | -------------------------------------------------------------------------------- /test/AppTest/Action/HomePageActionTest.php: -------------------------------------------------------------------------------- 1 | router = $this->prophesize(RouterInterface::class); 18 | } 19 | 20 | public function testResponse() 21 | { 22 | $homePage = new HomePageAction($this->router->reveal(), null); 23 | $response = $homePage(new ServerRequest(['/']), new Response(), function () { 24 | }); 25 | 26 | $this->assertTrue($response instanceof Response); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/AppTest/Action/HomePageFactoryTest.php: -------------------------------------------------------------------------------- 1 | container = $this->prophesize(ContainerInterface::class); 19 | $router = $this->prophesize(RouterInterface::class); 20 | 21 | $this->container->get(RouterInterface::class)->willReturn($router); 22 | } 23 | 24 | public function testFactoryWithoutTemplate() 25 | { 26 | $factory = new HomePageFactory(); 27 | $this->container->has(TemplateRendererInterface::class)->willReturn(false); 28 | 29 | $this->assertTrue($factory instanceof HomePageFactory); 30 | 31 | $homePage = $factory($this->container->reveal()); 32 | 33 | $this->assertTrue($homePage instanceof HomePageAction); 34 | } 35 | 36 | public function testFactoryWithTemplate() 37 | { 38 | $factory = new HomePageFactory(); 39 | $this->container->has(TemplateRendererInterface::class)->willReturn(true); 40 | $this->container 41 | ->get(TemplateRendererInterface::class) 42 | ->willReturn($this->prophesize(TemplateRendererInterface::class)); 43 | 44 | $this->assertTrue($factory instanceof HomePageFactory); 45 | 46 | $homePage = $factory($this->container->reveal()); 47 | 48 | $this->assertTrue($homePage instanceof HomePageAction); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/AppTest/Action/PingActionTest.php: -------------------------------------------------------------------------------- 1 | getBody()); 17 | 18 | $this->assertTrue($response instanceof Response); 19 | $this->assertTrue($response instanceof Response\JsonResponse); 20 | $this->assertTrue(isset($json->ack)); 21 | } 22 | } 23 | --------------------------------------------------------------------------------