├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── bin └── clear-config-cache.php ├── composer.json ├── config ├── .gitignore ├── autoload │ ├── .gitignore │ ├── dependencies.global.php │ ├── development.local.php.dist │ ├── local.php.dist │ └── zend-expressive.global.php ├── config.php ├── development.config.php.dist └── pipeline.php ├── data ├── .gitignore └── cache │ └── .gitkeep ├── docs ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── SUPPORT.md ├── phpcs.xml ├── phpcs.xml.dist ├── phpstan.installer.neon ├── phpunit.xml.dist ├── public ├── .htaccess └── index.php ├── src ├── App │ ├── src │ │ └── Handler │ │ │ ├── HomePageHandler.php │ │ │ ├── HomePageHandlerFactory.php │ │ │ └── PingHandler.php │ └── templates │ │ └── .gitkeep └── ExpressiveInstaller │ ├── OptionalPackages.php │ ├── Resources │ ├── config │ │ ├── container-aura-di.php │ │ ├── container-auryn.php │ │ ├── container-php-di.php │ │ ├── container-pimple.php │ │ ├── container-sf-di.php │ │ ├── container-zend-servicemanager.php │ │ ├── error-handler-whoops.php │ │ ├── routes-full.php │ │ └── routes-minimal.php │ ├── src │ │ ├── ConfigProvider.flat.php │ │ └── ConfigProvider.modular.php │ └── templates │ │ ├── plates │ │ ├── 404.phtml │ │ ├── error.phtml │ │ ├── home-page.phtml │ │ └── layout.phtml │ │ ├── twig │ │ ├── 404.html.twig │ │ ├── error.html.twig │ │ ├── home-page.html.twig │ │ └── layout.html.twig │ │ └── zend-view │ │ ├── 404.phtml │ │ ├── error.phtml │ │ ├── home-page.phtml │ │ └── layout.phtml │ └── config.php └── test ├── AppTest └── Handler │ ├── HomePageHandlerFactoryTest.php │ ├── HomePageHandlerTest.php │ └── PingHandlerTest.php └── ExpressiveInstallerTest ├── AddPackageTest.php ├── ContainersTest.php ├── CopyResourceTest.php ├── ErrorHandlerTest.php ├── OptionalPackagesTestCase.php ├── ProcessAnswersTest.php ├── ProjectSandboxTrait.php ├── PromptForOptionalPackagesTest.php ├── RemoveDevDependenciesTest.php ├── RemoveInstallerTest.php ├── RemoveLineFromStringTest.php ├── RequestInstallTypeTest.php ├── RoutersTest.php ├── SetupDataAndCacheDirTest.php ├── SetupDefaultAppTest.php ├── TemplateRenderersTest.php └── UpdateRootPackageTest.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | coverage_clover: clover.xml 2 | json_path: coveralls-upload.json 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.phar 3 | composer.lock 4 | clover.xml 5 | coveralls-upload.json 6 | phpunit.xml 7 | vendor/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | cache: 4 | directories: 5 | - $HOME/.composer/cache 6 | 7 | env: 8 | global: 9 | - COMPOSER_ARGS="--no-interaction --no-scripts" 10 | - COVERAGE_DEPS="php-coveralls/php-coveralls" 11 | 12 | matrix: 13 | include: 14 | - php: 7.1 15 | env: 16 | - DEPS=lowest 17 | - php: 7.1 18 | env: 19 | - DEPS=latest 20 | - CS_CHECK=true 21 | - PHPSTAN_CHECK=true 22 | - TEST_COVERAGE=true 23 | - php: 7.2 24 | env: 25 | - DEPS=lowest 26 | - php: 7.2 27 | env: 28 | - DEPS=latest 29 | - php: 7.3 30 | env: 31 | - DEPS=lowest 32 | - php: 7.3 33 | env: 34 | - DEPS=latest 35 | 36 | before_install: 37 | - if [[ $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi 38 | 39 | install: 40 | - if [[ $DEPS == 'latest' ]]; then travis_retry composer install $COMPOSER_ARGS ; fi 41 | - if [[ $DEPS == 'lowest' ]]; then travis_retry composer install $COMPOSER_ARGS; travis_retry composer update $COMPOSER_ARGS --prefer-lowest --prefer-stable ; fi 42 | - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer require --dev $COMPOSER_ARGS $COVERAGE_DEPS ; fi 43 | - stty cols 120 && composer show 44 | 45 | script: 46 | - if [[ $TEST_COVERAGE == 'true' ]]; then composer test-coverage ; else composer test ; fi 47 | - if [[ $CS_CHECK == 'true' ]]; then composer cs-check ; fi 48 | - if [[ $PHPSTAN_CHECK == 'true' ]]; then vendor/bin/phpstan analyze -l max -c ./phpstan.installer.neon ./src ./config ; fi 49 | 50 | after_script: 51 | - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry vendor/bin/php-coveralls -v ; fi 52 | 53 | notifications: 54 | email: false 55 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2018, Zend Technologies USA, Inc. 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, 6 | are permitted provided that the following conditions are met: 7 | 8 | - Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | - Redistributions in binary form must reproduce the above copyright notice, this 12 | list of conditions and the following disclaimer in the documentation and/or 13 | other materials provided with the distribution. 14 | 15 | - Neither the name of Zend Technologies USA, Inc. nor the names of its 16 | contributors may be used to endorse or promote products derived from this 17 | software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 23 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Expressive Skeleton and Installer 2 | 3 | > ## Repository abandoned 2019-12-31 4 | > 5 | > This repository has moved to [mezzio/mezzio-skeleton](https://github.com/mezzio/mezzio-skeleton). 6 | 7 | [![Build Status](https://secure.travis-ci.org/zendframework/zend-expressive-skeleton.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-expressive-skeleton) 8 | [![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) 9 | 10 | *Begin developing PSR-15 middleware applications in seconds!* 11 | 12 | [zend-expressive](https://github.com/zendframework/zend-expressive) builds on 13 | [zend-stratigility](https://github.com/zendframework/zend-stratigility) to 14 | provide a minimalist PSR-15 middleware framework for PHP with routing, DI 15 | container, optional templating, and optional error handling capabilities. 16 | 17 | This installer will setup a skeleton application based on zend-expressive by 18 | choosing optional packages based on user input as demonstrated in the following 19 | screenshot: 20 | 21 | ![screenshot-installer](https://cloud.githubusercontent.com/assets/459648/10410494/16bdc674-6f6d-11e5-8190-3c1466e93361.png) 22 | 23 | The user selected packages are saved into `composer.json` so that everyone else 24 | working on the project have the same packages installed. Configuration files and 25 | templates are prepared for first use. The installer command is removed from 26 | `composer.json` after setup succeeded, and all installer related files are 27 | removed. 28 | 29 | ## Getting Started 30 | 31 | Start your new Expressive project with composer: 32 | 33 | ```bash 34 | $ composer create-project zendframework/zend-expressive-skeleton 35 | ``` 36 | 37 | After choosing and installing the packages you want, go to the 38 | `` and start PHP's built-in web server to verify installation: 39 | 40 | ```bash 41 | $ composer run --timeout=0 serve 42 | ``` 43 | 44 | You can then browse to http://localhost:8080. 45 | 46 | > ### Linux users 47 | > 48 | > On PHP versions prior to 7.1.14 and 7.2.2, this command might not work as 49 | > expected due to a bug in PHP that only affects linux environments. In such 50 | > scenarios, you will need to start the [built-in web 51 | > server](http://php.net/manual/en/features.commandline.webserver.php) yourself, 52 | > using the following command: 53 | > 54 | > ```bash 55 | > $ php -S 0.0.0.0:8080 -t public/ public/index.php 56 | > ``` 57 | 58 | > ### Setting a timeout 59 | > 60 | > Composer commands time out after 300 seconds (5 minutes). On Linux-based 61 | > systems, the `php -S` command that `composer serve` spawns continues running 62 | > as a background process, but on other systems halts when the timeout occurs. 63 | > 64 | > As such, we recommend running the `serve` script using a timeout. This can 65 | > be done by using `composer run` to execute the `serve` script, with a 66 | > `--timeout` option. When set to `0`, as in the previous example, no timeout 67 | > will be used, and it will run until you cancel the process (usually via 68 | > `Ctrl-C`). Alternately, you can specify a finite timeout; as an example, 69 | > the following will extend the timeout to a full day: 70 | > 71 | > ```bash 72 | > $ composer run --timeout=86400 serve 73 | > ``` 74 | 75 | ## Troubleshooting 76 | 77 | If the installer fails during the ``composer create-project`` phase, please go 78 | through the following list before opening a new issue. Most issues we have seen 79 | so far can be solved by `self-update` and `clear-cache`. 80 | 81 | 1. Be sure to work with the latest version of composer by running `composer self-update`. 82 | 2. Try clearing Composer's cache by running `composer clear-cache`. 83 | 84 | If neither of the above help, you might face more serious issues: 85 | 86 | - Info about the [zlib_decode error](https://github.com/composer/composer/issues/4121). 87 | - Info and solutions for [composer degraded mode](https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode). 88 | 89 | ## Application Development Mode Tool 90 | 91 | This skeleton comes with [zf-development-mode](https://github.com/zfcampus/zf-development-mode). 92 | It provides a composer script to allow you to enable and disable development mode. 93 | 94 | ### To enable development mode 95 | 96 | **Note:** Do NOT run development mode on your production server! 97 | 98 | ```bash 99 | $ composer development-enable 100 | ``` 101 | 102 | **Note:** Enabling development mode will also clear your configuration cache, to 103 | allow safely updating dependencies and ensuring any new configuration is picked 104 | up by your application. 105 | 106 | ### To disable development mode 107 | 108 | ```bash 109 | $ composer development-disable 110 | ``` 111 | 112 | ### Development mode status 113 | 114 | ```bash 115 | $ composer development-status 116 | ``` 117 | 118 | ## Configuration caching 119 | 120 | By default, the skeleton will create a configuration cache in 121 | `data/config-cache.php`. When in development mode, the configuration cache is 122 | disabled, and switching in and out of development mode will remove the 123 | configuration cache. 124 | 125 | You may need to clear the configuration cache in production when deploying if 126 | you deploy to the same directory. You may do so using the following: 127 | 128 | ```bash 129 | $ composer clear-config-cache 130 | ``` 131 | 132 | You may also change the location of the configuration cache itself by editing 133 | the `config/config.php` file and changing the `config_cache_path` entry of the 134 | local `$cacheConfig` variable. 135 | 136 | ## Skeleton Development 137 | 138 | This section applies only if you cloned this repo with `git clone`, not when you 139 | installed expressive with `composer create-project ...`. 140 | 141 | If you want to run tests against the installer, you need to clone this repo and 142 | setup all dependencies with composer. Make sure you **prevent composer running 143 | scripts** with `--no-scripts`, otherwise it will remove the installer and all 144 | tests. 145 | 146 | ```bash 147 | $ composer update --no-scripts 148 | $ composer test 149 | ``` 150 | 151 | Please note that the installer tests remove installed config files and templates 152 | before and after running the tests. 153 | 154 | Before contributing read [the contributing guide](docs/CONTRIBUTING.md). 155 | -------------------------------------------------------------------------------- /bin/clear-config-cache.php: -------------------------------------------------------------------------------- 1 | [ 10 | // Use 'aliases' to alias a service name to another service. The 11 | // key is the alias name, the value is the service to which it points. 12 | 'aliases' => [ 13 | // Fully\Qualified\ClassOrInterfaceName::class => Fully\Qualified\ClassName::class, 14 | ], 15 | // Use 'invokables' for constructor-less services, or services that do 16 | // not require arguments to the constructor. Map a service name to the 17 | // class name. 18 | 'invokables' => [ 19 | // Fully\Qualified\InterfaceName::class => Fully\Qualified\ClassName::class, 20 | ], 21 | // Use 'factories' for services provided by callbacks/factory classes. 22 | 'factories' => [ 23 | // Fully\Qualified\ClassName::class => Fully\Qualified\FactoryName::class, 24 | ], 25 | ], 26 | ]; 27 | -------------------------------------------------------------------------------- /config/autoload/development.local.php.dist: -------------------------------------------------------------------------------- 1 | true, 13 | 14 | // Enable debugging; typically used to provide debugging information within templates. 15 | 'debug' => false, 16 | 17 | 'zend-expressive' => [ 18 | // Provide templates for the error handling middleware to use when 19 | // generating responses. 20 | 'error_handler' => [ 21 | 'template_404' => 'error::404', 22 | 'template_error' => 'error::error', 23 | ], 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | 'data/cache/config-cache.php', 13 | ]; 14 | 15 | $aggregator = new ConfigAggregator([ 16 | // Include cache configuration 17 | new ArrayProvider($cacheConfig), 18 | 19 | \Zend\Expressive\Helper\ConfigProvider::class, 20 | \Zend\Expressive\ConfigProvider::class, 21 | \Zend\Expressive\Router\ConfigProvider::class, 22 | 23 | // Swoole config to overwrite some services (if installed) 24 | class_exists(\Zend\Expressive\Swoole\ConfigProvider::class) 25 | ? \Zend\Expressive\Swoole\ConfigProvider::class 26 | : function(){ return[]; }, 27 | 28 | // Default App module config 29 | App\ConfigProvider::class, 30 | 31 | // Load application config in a pre-defined order in such a way that local settings 32 | // overwrite global settings. (Loaded as first to last): 33 | // - `global.php` 34 | // - `*.global.php` 35 | // - `local.php` 36 | // - `*.local.php` 37 | new PhpFileProvider(realpath(__DIR__) . '/autoload/{{,*.}global,{,*.}local}.php'), 38 | 39 | // Load development config if it exists 40 | new PhpFileProvider(realpath(__DIR__) . '/development.config.php'), 41 | ], $cacheConfig['config_cache_path']); 42 | 43 | return $aggregator->getMergedConfig(); 44 | -------------------------------------------------------------------------------- /config/development.config.php.dist: -------------------------------------------------------------------------------- 1 | true, 29 | ConfigAggregator::ENABLE_CACHE => false, 30 | ]; 31 | -------------------------------------------------------------------------------- /config/pipeline.php: -------------------------------------------------------------------------------- 1 | pipe(ErrorHandler::class); 25 | $app->pipe(ServerUrlMiddleware::class); 26 | 27 | // Pipe more middleware here that you want to execute on every request: 28 | // - bootstrapping 29 | // - pre-conditions 30 | // - modifications to outgoing responses 31 | // 32 | // Piped Middleware may be either callables or service names. Middleware may 33 | // also be passed as an array; each item in the array must resolve to 34 | // middleware eventually (i.e., callable or service name). 35 | // 36 | // Middleware can be attached to specific paths, allowing you to mix and match 37 | // applications under a common domain. The handlers in each middleware 38 | // attached this way will see a URI with the matched path segment removed. 39 | // 40 | // i.e., path of "/api/member/profile" only passes "/member/profile" to $apiMiddleware 41 | // - $app->pipe('/api', $apiMiddleware); 42 | // - $app->pipe('/docs', $apiDocMiddleware); 43 | // - $app->pipe('/files', $filesMiddleware); 44 | 45 | // Register the routing middleware in the middleware pipeline. 46 | // This middleware registers the Zend\Expressive\Router\RouteResult request attribute. 47 | $app->pipe(RouteMiddleware::class); 48 | 49 | // The following handle routing failures for common conditions: 50 | // - HEAD request but no routes answer that method 51 | // - OPTIONS request but no routes answer that method 52 | // - method not allowed 53 | // Order here matters; the MethodNotAllowedMiddleware should be placed 54 | // after the Implicit*Middleware. 55 | $app->pipe(ImplicitHeadMiddleware::class); 56 | $app->pipe(ImplicitOptionsMiddleware::class); 57 | $app->pipe(MethodNotAllowedMiddleware::class); 58 | 59 | // Seed the UrlHelper with the routing results: 60 | $app->pipe(UrlHelperMiddleware::class); 61 | 62 | // Add more middleware here that needs to introspect the routing results; this 63 | // might include: 64 | // 65 | // - route-based authentication 66 | // - route-based validation 67 | // - etc. 68 | 69 | // Register the dispatch middleware in the middleware pipeline 70 | $app->pipe(DispatchMiddleware::class); 71 | 72 | // At this point, if no Response is returned by any middleware, the 73 | // NotFoundHandler kicks in; alternately, you can provide other fallback 74 | // middleware to execute. 75 | $app->pipe(NotFoundHandler::class); 76 | }; 77 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !cache 3 | !cache/.gitkeep 4 | !.gitignore 5 | -------------------------------------------------------------------------------- /data/cache/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendframework/zend-expressive-skeleton/11f025c68a7a1d326a777df6a74b4f46a4d9b62e/data/cache/.gitkeep -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | This 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 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | ## RESOURCES 4 | 5 | If you wish to contribute to this project, please be sure to 6 | read/subscribe to the following resources: 7 | 8 | - [Coding Standards](https://github.com/zendframework/zend-coding-standard) 9 | - [Forums](https://discourse.zendframework.com/c/contributors) 10 | - [Slack](https://zendframework-slack.herokuapp.com) 11 | - [Code of Conduct](CODE_OF_CONDUCT.md) 12 | 13 | If you are working on new features or refactoring 14 | [create a proposal](https://github.com/zendframework/zend-expressive-skeleton/issues/new). 15 | 16 | ## RUNNING TESTS 17 | 18 | To run tests: 19 | 20 | - Clone the repository: 21 | 22 | ```console 23 | $ git clone git://github.com/zendframework/zend-expressive-skeleton.git 24 | $ cd zend-expressive-skeleton 25 | ``` 26 | 27 | - Install dependencies via composer: 28 | 29 | ```console 30 | $ composer install --no-scripts 31 | ``` 32 | 33 | **NOTE:** Without the `--no-scripts` flag, composer will invoke the installer, 34 | which in turn will remove itself when complete. 35 | 36 | If you don't have `composer` installed, please download it from https://getcomposer.org/download/ 37 | 38 | - Run the tests using the "test" command shipped in the `composer.json`: 39 | 40 | ```console 41 | $ composer test 42 | ``` 43 | 44 | You can turn on conditional tests with the `phpunit.xml` file. 45 | To do so: 46 | 47 | - Copy `phpunit.xml.dist` file to `phpunit.xml` 48 | - Edit `phpunit.xml` to enable any specific functionality you 49 | want to test, as well as to provide test values to utilize. 50 | 51 | ## Running Coding Standards Checks 52 | 53 | First, ensure you've installed dependencies via composer, per the previous 54 | section on running tests. 55 | 56 | To run CS checks only: 57 | 58 | ```console 59 | $ composer cs-check 60 | ``` 61 | 62 | To attempt to automatically fix common CS issues: 63 | 64 | ```console 65 | $ composer cs-fix 66 | ``` 67 | 68 | If the above fixes any CS issues, please re-run the tests to ensure 69 | they pass, and make sure you add and commit the changes after verification. 70 | 71 | ## Recommended Workflow for Contributions 72 | 73 | Your first step is to establish a public repository from which we can 74 | pull your work into the master repository. We recommend using 75 | [GitHub](https://github.com), as that is where the component is already hosted. 76 | 77 | 1. Setup a [GitHub account](https://github.com/), if you haven't yet 78 | 2. Fork the repository (https://github.com/zendframework/zend-expressive-skeleton) 79 | 3. Clone the canonical repository locally and enter it. 80 | 81 | ```console 82 | $ git clone git://github.com/zendframework/zend-expressive-skeleton.git 83 | $ cd zend-expressive-skeleton 84 | ``` 85 | 86 | 4. Add a remote to your fork; substitute your GitHub username in the command 87 | below. 88 | 89 | ```console 90 | $ git remote add {username} git@github.com:{username}/zend-expressive-skeleton.git 91 | $ git fetch {username} 92 | ``` 93 | 94 | ### Keeping Up-to-Date 95 | 96 | Periodically, you should update your fork or personal repository to 97 | match the canonical ZF repository. Assuming you have setup your local repository 98 | per the instructions above, you can do the following: 99 | 100 | 101 | ```console 102 | $ git checkout master 103 | $ git fetch origin 104 | $ git rebase origin/master 105 | # OPTIONALLY, to keep your remote up-to-date - 106 | $ git push {username} master:master 107 | ``` 108 | 109 | If you're tracking other branches -- for example, the "develop" branch, where 110 | new feature development occurs -- you'll want to do the same operations for that 111 | branch; simply substitute "develop" for "master". 112 | 113 | ### Working on a patch 114 | 115 | We recommend you do each new feature or bugfix in a new branch. This simplifies 116 | the task of code review as well as the task of merging your changes into the 117 | canonical repository. 118 | 119 | A typical workflow will then consist of the following: 120 | 121 | 1. Create a new local branch based off either your master or develop branch. 122 | 2. Switch to your new local branch. (This step can be combined with the 123 | previous step with the use of `git checkout -b`.) 124 | 3. Do some work, commit, repeat as necessary. 125 | 4. Push the local branch to your remote repository. 126 | 5. Send a pull request. 127 | 128 | The mechanics of this process are actually quite trivial. Below, we will 129 | create a branch for fixing an issue in the tracker. 130 | 131 | ```console 132 | $ git checkout -b hotfix/9295 133 | Switched to a new branch 'hotfix/9295' 134 | ``` 135 | 136 | ... do some work ... 137 | 138 | 139 | ```console 140 | $ git commit 141 | ``` 142 | 143 | ... write your log message ... 144 | 145 | 146 | ```console 147 | $ git push {username} hotfix/9295:hotfix/9295 148 | Counting objects: 38, done. 149 | Delta compression using up to 2 threads. 150 | Compression objects: 100% (18/18), done. 151 | Writing objects: 100% (20/20), 8.19KiB, done. 152 | Total 20 (delta 12), reused 0 (delta 0) 153 | To ssh://git@github.com/{username}/zend-expressive-skeleton.git 154 | b5583aa..4f51698 HEAD -> master 155 | ``` 156 | 157 | To send a pull request, you have two options. 158 | 159 | If using GitHub, you can do the pull request from there. Navigate to 160 | your repository, select the branch you just created, and then select the 161 | "Pull Request" button in the upper right. Select the user/organization 162 | "zendframework" (or whatever the upstream organization is) as the recipient. 163 | 164 | #### What branch to issue the pull request against? 165 | 166 | Which branch should you issue a pull request against? 167 | 168 | - For fixes against the stable release, issue the pull request against the 169 | "master" branch. 170 | - For new features, or fixes that introduce new elements to the public API (such 171 | as new public methods or properties), issue the pull request against the 172 | "develop" branch. 173 | 174 | ### Branch Cleanup 175 | 176 | As you might imagine, if you are a frequent contributor, you'll start to 177 | get a ton of branches both locally and on your remote. 178 | 179 | Once you know that your changes have been accepted to the master 180 | repository, we suggest doing some cleanup of these branches. 181 | 182 | - Local branch cleanup 183 | 184 | ```console 185 | $ git branch -d 186 | ``` 187 | 188 | - Remote branch removal 189 | 190 | ```console 191 | $ git push {username} : 192 | ``` 193 | -------------------------------------------------------------------------------- /docs/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] I was not able to find an [open](https://github.com/zendframework/zend-expressive-skeleton/issues?q=is%3Aopen) or [closed](https://github.com/zendframework/zend-expressive-skeleton/issues?q=is%3Aclosed) issue matching what I'm seeing. 2 | - [ ] This is not a question. (Questions should be asked on [chat](https://zendframework.slack.com/) ([Signup for here](https://zendframework-slack.herokuapp.com/)) or our [forums](https://discourse.zendframework.com/).) 3 | 4 | Provide a narrative description of what you are trying to accomplish. 5 | 6 | ### Code to reproduce the issue 7 | 8 | 9 | 10 | ```php 11 | 12 | ``` 13 | 14 | ### Expected results 15 | 16 | 17 | 18 | ### Actual results 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Provide a narrative description of what you are trying to accomplish: 2 | 3 | - [ ] Are you fixing a bug? 4 | - [ ] Detail how the bug is invoked currently. 5 | - [ ] Detail the original, incorrect behavior. 6 | - [ ] Detail the new, expected behavior. 7 | - [ ] Base your feature on the `master` branch, and submit against that branch. 8 | - [ ] Add a regression test that demonstrates the bug, and proves the fix. 9 | - [ ] Add a `CHANGELOG.md` entry for the fix. 10 | 11 | - [ ] Are you creating a new feature? 12 | - [ ] Why is the new feature needed? What purpose does it serve? 13 | - [ ] How will users use the new feature? 14 | - [ ] Base your feature on the `develop` branch, and submit against that branch. 15 | - [ ] Add only one feature per pull request; split multiple features over multiple pull requests 16 | - [ ] Add tests for the new feature. 17 | - [ ] Add documentation for the new feature. 18 | - [ ] Add a `CHANGELOG.md` entry for the new feature. 19 | 20 | - [ ] Is this related to quality assurance? 21 | 22 | 23 | - [ ] Is this related to documentation? 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Getting Support 2 | 3 | Zend Framework offers three support channels: 4 | 5 | - For real-time questions, use our 6 | [chat](https://zendframework-slack.herokuapp.com) 7 | - For detailed questions (e.g., those requiring examples) use our 8 | [forums](https://discourse.zendframework.com/c/questions/{category}) 9 | - To report issues, use this repository's 10 | [issue tracker](https://github.com/zendframework/zend-expressive-skeleton/issues/new) 11 | 12 | **DO NOT** use the issue tracker to ask questions; use chat or the forums for 13 | that. Questions posed to the issue tracker will be closed. 14 | 15 | When reporting an issue, please include the following details: 16 | 17 | - A narrative description of what you are trying to accomplish. 18 | - The minimum code necessary to reproduce the issue. 19 | - The expected results of exercising that code. 20 | - The actual results received. 21 | 22 | We may ask for additional details: what version of the library you are using, 23 | and what PHP version was used to reproduce the issue. 24 | 25 | You may also submit a failing test case as a pull request. 26 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | src 7 | test 8 | 9 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Expressive Skeleton coding standard 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | src 20 | 21 | -------------------------------------------------------------------------------- /phpstan.installer.neon: -------------------------------------------------------------------------------- 1 | # zend-expressive-skeleton specific 2 | includes: 3 | - vendor/phpstan/phpstan-strict-rules/rules.neon 4 | parameters: 5 | fileExtensions: 6 | # Standard php files and .dist config files 7 | - php 8 | - dist 9 | excludes_analyse: 10 | - src/ExpressiveInstaller/Resources/src/ConfigProvider.flat.php 11 | - src/ExpressiveInstaller/Resources/src/ConfigProvider.modular.php 12 | reportUnmatchedIgnoredErrors: true 13 | ignoreErrors: 14 | - '#Class App\\ConfigProvider not found#' 15 | - '#Class Zend\\Expressive\\Swoole\\ConfigProvider not found#' 16 | -------------------------------------------------------------------------------- /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 allows authentication to work with fast-cgi 3 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 4 | # The following rule tells Apache that if the requested filename 5 | # exists, simply serve it. 6 | RewriteCond %{REQUEST_FILENAME} -s [OR] 7 | RewriteCond %{REQUEST_FILENAME} -l [OR] 8 | RewriteCond %{REQUEST_FILENAME} -d 9 | RewriteRule ^.*$ - [NC,L] 10 | 11 | # The following rewrites all other queries to index.php. The 12 | # condition ensures that if you are using Apache aliases to do 13 | # mass virtual hosting, the base path will be prepended to 14 | # allow proper resolution of the index.php file; it will work 15 | # in non-aliased environments as well, providing a safe, one-size 16 | # fits all solution. 17 | RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$ 18 | RewriteRule ^(.*) - [E=BASE:%1] 19 | RewriteRule ^(.*)$ %{ENV:BASE}index.php [NC,L] 20 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | get(\Zend\Expressive\Application::class); 22 | $factory = $container->get(\Zend\Expressive\MiddlewareFactory::class); 23 | 24 | // Execute programmatic/declarative middleware pipeline and routing 25 | // configuration statements 26 | (require 'config/pipeline.php')($app, $factory, $container); 27 | (require 'config/routes.php')($app, $factory, $container); 28 | 29 | $app->run(); 30 | })(); 31 | -------------------------------------------------------------------------------- /src/App/src/Handler/HomePageHandler.php: -------------------------------------------------------------------------------- 1 | containerName = $containerName; 35 | $this->router = $router; 36 | $this->template = $template; 37 | } 38 | 39 | public function handle(ServerRequestInterface $request) : ResponseInterface 40 | { 41 | if ($this->template === null) { 42 | return new JsonResponse([ 43 | 'welcome' => 'Congratulations! You have installed the zend-expressive skeleton application.', 44 | 'docsUrl' => 'https://docs.zendframework.com/zend-expressive/', 45 | ]); 46 | } 47 | 48 | $data = []; 49 | 50 | switch ($this->containerName) { 51 | case 'Aura\Di\Container': 52 | $data['containerName'] = 'Aura.Di'; 53 | $data['containerDocs'] = 'http://auraphp.com/packages/2.x/Di.html'; 54 | break; 55 | case 'Pimple\Container': 56 | $data['containerName'] = 'Pimple'; 57 | $data['containerDocs'] = 'https://pimple.symfony.com/'; 58 | break; 59 | case 'Zend\ServiceManager\ServiceManager': 60 | $data['containerName'] = 'Zend Servicemanager'; 61 | $data['containerDocs'] = 'https://docs.zendframework.com/zend-servicemanager/'; 62 | break; 63 | case 'Auryn\Injector': 64 | $data['containerName'] = 'Auryn'; 65 | $data['containerDocs'] = 'https://github.com/rdlowrey/Auryn'; 66 | break; 67 | case 'Symfony\Component\DependencyInjection\ContainerBuilder': 68 | $data['containerName'] = 'Symfony DI Container'; 69 | $data['containerDocs'] = 'https://symfony.com/doc/current/service_container.html'; 70 | break; 71 | case 'Zend\DI\Config\ContainerWrapper': 72 | case 'DI\Container': 73 | $data['containerName'] = 'PHP-DI'; 74 | $data['containerDocs'] = 'http://php-di.org'; 75 | break; 76 | } 77 | 78 | if ($this->router instanceof Router\AuraRouter) { 79 | $data['routerName'] = 'Aura.Router'; 80 | $data['routerDocs'] = 'http://auraphp.com/packages/2.x/Router.html'; 81 | } elseif ($this->router instanceof Router\FastRouteRouter) { 82 | $data['routerName'] = 'FastRoute'; 83 | $data['routerDocs'] = 'https://github.com/nikic/FastRoute'; 84 | } elseif ($this->router instanceof Router\ZendRouter) { 85 | $data['routerName'] = 'Zend Router'; 86 | $data['routerDocs'] = 'https://docs.zendframework.com/zend-router/'; 87 | } 88 | 89 | if ($this->template instanceof PlatesRenderer) { 90 | $data['templateName'] = 'Plates'; 91 | $data['templateDocs'] = 'http://platesphp.com/'; 92 | } elseif ($this->template instanceof TwigRenderer) { 93 | $data['templateName'] = 'Twig'; 94 | $data['templateDocs'] = 'http://twig.sensiolabs.org/documentation'; 95 | } elseif ($this->template instanceof ZendViewRenderer) { 96 | $data['templateName'] = 'Zend View'; 97 | $data['templateDocs'] = 'https://docs.zendframework.com/zend-view/'; 98 | } 99 | 100 | return new HtmlResponse($this->template->render('app::home-page', $data)); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/App/src/Handler/HomePageHandlerFactory.php: -------------------------------------------------------------------------------- 1 | get(RouterInterface::class); 19 | $template = $container->has(TemplateRendererInterface::class) 20 | ? $container->get(TemplateRendererInterface::class) 21 | : null; 22 | 23 | return new HomePageHandler(get_class($container), $router, $template); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/App/src/Handler/PingHandler.php: -------------------------------------------------------------------------------- 1 | time()]); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/App/templates/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zendframework/zend-expressive-skeleton/11f025c68a7a1d326a777df6a74b4f46a4d9b62e/src/App/templates/.gitkeep -------------------------------------------------------------------------------- /src/ExpressiveInstaller/OptionalPackages.php: -------------------------------------------------------------------------------- 1 | [^:]+\/[^:]+)([:]*)(?P.*)$/'; 46 | 47 | /** 48 | * @const string Configuration file lines related to registering the default 49 | * App module configuration. 50 | */ 51 | public const APP_MODULE_CONFIG = ' 52 | // Default App module config 53 | App\ConfigProvider::class, 54 | 55 | '; 56 | 57 | /** 58 | * Assets to remove during cleanup. 59 | * 60 | * @var string[] 61 | */ 62 | private $assetsToRemove = [ 63 | '.coveralls.yml', 64 | '.travis.yml', 65 | 'CHANGELOG.md', 66 | 'phpcs.xml', 67 | 'phpstan.installer.neon', 68 | 'src/App/templates/.gitkeep', 69 | ]; 70 | 71 | /** 72 | * @var array 73 | */ 74 | private $config; 75 | 76 | /** 77 | * @var Composer 78 | */ 79 | private $composer; 80 | 81 | /** 82 | * @var array 83 | */ 84 | private $composerDefinition; 85 | 86 | /** 87 | * @var JsonFile 88 | */ 89 | private $composerJson; 90 | 91 | /** 92 | * @var Link[] 93 | */ 94 | private $composerRequires; 95 | 96 | /** 97 | * @var Link[] 98 | */ 99 | private $composerDevRequires; 100 | 101 | /** 102 | * @var string[] Dev dependencies to remove after install is complete 103 | */ 104 | private $devDependencies = [ 105 | 'composer/composer', 106 | 'elie29/zend-phpdi-config', 107 | 'filp/whoops', 108 | 'jsoumelidis/zend-sf-di-config', 109 | 'mikey179/vfsstream', 110 | 'northwoods/container', 111 | "phpstan/phpstan", 112 | "phpstan/phpstan-strict-rules", 113 | 'zendframework/zend-auradi-config', 114 | 'zendframework/zend-coding-standard', 115 | 'zendframework/zend-expressive-aurarouter', 116 | 'zendframework/zend-expressive-fastroute', 117 | 'zendframework/zend-expressive-platesrenderer', 118 | 'zendframework/zend-expressive-twigrenderer', 119 | 'zendframework/zend-expressive-zendrouter', 120 | 'zendframework/zend-expressive-zendviewrenderer', 121 | 'zendframework/zend-pimple-config', 122 | 'zendframework/zend-servicemanager', 123 | ]; 124 | 125 | /** 126 | * @var string Path to this file. 127 | */ 128 | private $installerSource; 129 | 130 | /** 131 | * @var string Installation type selected. 132 | */ 133 | private $installType = self::INSTALL_FLAT; 134 | 135 | /** 136 | * @var IOInterface 137 | */ 138 | private $io; 139 | 140 | /** 141 | * @var string 142 | */ 143 | private $projectRoot; 144 | 145 | /** 146 | * @var RootPackageInterface 147 | */ 148 | private $rootPackage; 149 | 150 | /** 151 | * @var int[] 152 | */ 153 | private $stabilityFlags; 154 | 155 | /** 156 | * Install command: choose packages and provide configuration. 157 | * 158 | * Prompts users for package selections, and copies in package-specific 159 | * configuration when known. 160 | * 161 | * Updates the composer.json with the package selections, and removes the 162 | * install and update commands on completion. 163 | * 164 | * @codeCoverageIgnore 165 | */ 166 | public static function install(Event $event) : void 167 | { 168 | $installer = new self($event->getIO(), $event->getComposer()); 169 | 170 | $installer->io->write('Setting up optional packages'); 171 | 172 | $installer->setupDataAndCacheDir(); 173 | $installer->removeDevDependencies(); 174 | $installer->setInstallType($installer->requestInstallType()); 175 | $installer->setupDefaultApp(); 176 | $installer->promptForOptionalPackages(); 177 | $installer->updateRootPackage(); 178 | $installer->removeInstallerFromDefinition(); 179 | $installer->finalizePackage(); 180 | } 181 | 182 | public function __construct(IOInterface $io, Composer $composer, string $projectRoot = null) 183 | { 184 | $this->io = $io; 185 | $this->composer = $composer; 186 | 187 | // Get composer.json location 188 | $composerFile = Factory::getComposerFile(); 189 | 190 | // Calculate project root from composer.json, if necessary 191 | $this->projectRoot = $projectRoot ?: realpath(dirname($composerFile)); 192 | $this->projectRoot = rtrim($this->projectRoot, '/\\') . '/'; 193 | 194 | // Parse the composer.json 195 | $this->parseComposerDefinition($composer, $composerFile); 196 | 197 | // Get optional packages configuration 198 | $this->config = require __DIR__ . '/config.php'; 199 | 200 | // Source path for this file 201 | $this->installerSource = realpath(__DIR__) . '/'; 202 | } 203 | 204 | /** 205 | * Create data and cache directories, if not present. 206 | * 207 | * Also sets up appropriate permissions. 208 | */ 209 | public function setupDataAndCacheDir() : void 210 | { 211 | $this->io->write('Setup data and cache dir'); 212 | if (! is_dir($this->projectRoot . '/data/cache')) { 213 | mkdir($this->projectRoot . '/data/cache', 0775, true); 214 | chmod($this->projectRoot . '/data', 0775); 215 | } 216 | } 217 | 218 | /** 219 | * Cleanup development dependencies. 220 | * 221 | * The dev dependencies should be removed from the stability flags, 222 | * require-dev and the composer file. 223 | */ 224 | public function removeDevDependencies() : void 225 | { 226 | $this->io->write('Removing installer development dependencies'); 227 | foreach ($this->devDependencies as $devDependency) { 228 | unset($this->stabilityFlags[$devDependency]); 229 | unset($this->composerDevRequires[$devDependency]); 230 | unset($this->composerDefinition['require-dev'][$devDependency]); 231 | } 232 | } 233 | 234 | /** 235 | * Prompt for the installation type. 236 | * 237 | * @return string One of the INSTALL_ constants. 238 | */ 239 | public function requestInstallType() : string 240 | { 241 | $query = [ 242 | sprintf( 243 | "\n %s\n", 244 | 'What type of installation would you like?' 245 | ), 246 | " [1] Minimal (no default middleware, templates, or assets; configuration only)\n", 247 | " [2] Flat (flat source code structure; default selection)\n", 248 | " [3] Modular (modular source code structure; recommended)\n", 249 | ' Make your selection (2): ', 250 | ]; 251 | 252 | while (true) { 253 | $answer = $this->io->ask(implode($query), '2'); 254 | 255 | switch (true) { 256 | case ($answer === '1'): 257 | return self::INSTALL_MINIMAL; 258 | case ($answer === '2'): 259 | return self::INSTALL_FLAT; 260 | case ($answer === '3'): 261 | return self::INSTALL_MODULAR; 262 | default: 263 | // @codeCoverageIgnoreStart 264 | $this->io->write('Invalid answer'); 265 | // @codeCoverageIgnoreEnd 266 | } 267 | } 268 | } 269 | 270 | /** 271 | * Set the install type. 272 | */ 273 | public function setInstallType(string $installType) : void 274 | { 275 | $this->installType = 276 | in_array($installType, [ 277 | self::INSTALL_FLAT, 278 | self::INSTALL_MINIMAL, 279 | self::INSTALL_MODULAR, 280 | ], true) 281 | ? $installType 282 | : self::INSTALL_FLAT; 283 | } 284 | 285 | /** 286 | * Setup the default application structure. 287 | * 288 | * @throws RuntimeException if $installType is unknown 289 | */ 290 | public function setupDefaultApp() : void 291 | { 292 | switch ($this->installType) { 293 | case self::INSTALL_MINIMAL: 294 | $this->removeDefaultModule(); 295 | // no files to copy 296 | return; 297 | 298 | case self::INSTALL_FLAT: 299 | // Re-arrange files into a flat structure. 300 | $this->recursiveRmdir($this->projectRoot . '/src/App/templates'); 301 | rename($this->projectRoot . '/src/App/src/Handler', $this->projectRoot . '/src/App/Handler'); 302 | $this->recursiveRmdir($this->projectRoot . '/src/App/src'); 303 | 304 | // Re-define autoloading rules 305 | $this->composerDefinition['autoload']['psr-4']['App\\'] = 'src/App/'; 306 | break; 307 | 308 | case self::INSTALL_MODULAR: 309 | // Nothing additional to do 310 | break; 311 | 312 | default: 313 | throw new RuntimeException(sprintf( 314 | 'Invalid install type "%s"; this indicates a bug in the installer', 315 | $this->installType 316 | )); 317 | } 318 | 319 | foreach ($this->config['application'][$this->installType]['packages'] as $package => $constraint) { 320 | $this->addPackage($package, $constraint); 321 | } 322 | 323 | foreach ($this->config['application'][$this->installType]['resources'] as $resource => $target) { 324 | $this->copyResource($resource, $target); 325 | } 326 | } 327 | 328 | /** 329 | * Prompt for each optional installation package. 330 | * 331 | * @codeCoverageIgnore 332 | */ 333 | public function promptForOptionalPackages() : void 334 | { 335 | foreach ($this->config['questions'] as $questionName => $question) { 336 | $this->promptForOptionalPackage($questionName, $question); 337 | } 338 | } 339 | 340 | /** 341 | * Prompt for a single optional installation package. 342 | * 343 | * @param string $questionName Name of question 344 | * @param array $question Question details from configuration 345 | */ 346 | public function promptForOptionalPackage(string $questionName, array $question) : void 347 | { 348 | $defaultOption = $question['default'] ?? 1; 349 | if (isset($this->composerDefinition['extra']['optional-packages'][$questionName])) { 350 | // Skip question, it's already answered 351 | return; 352 | } 353 | 354 | // Get answer 355 | $answer = $this->askQuestion($question, $defaultOption); 356 | 357 | // Process answer 358 | $this->processAnswer($question, $answer); 359 | 360 | // Save user selected option 361 | $this->composerDefinition['extra']['optional-packages'][$questionName] = $answer; 362 | 363 | // Update composer definition 364 | $this->composerJson->write($this->composerDefinition); 365 | } 366 | 367 | /** 368 | * Update the root package based on current state. 369 | */ 370 | public function updateRootPackage() : void 371 | { 372 | $this->rootPackage->setRequires($this->composerRequires); 373 | $this->rootPackage->setDevRequires($this->composerDevRequires); 374 | $this->rootPackage->setStabilityFlags($this->stabilityFlags); 375 | $this->rootPackage->setAutoload($this->composerDefinition['autoload']); 376 | $this->rootPackage->setDevAutoload($this->composerDefinition['autoload-dev']); 377 | $this->rootPackage->setExtra($this->composerDefinition['extra'] ?? []); 378 | } 379 | 380 | /** 381 | * Remove the installer from the composer definition 382 | */ 383 | public function removeInstallerFromDefinition() : void 384 | { 385 | $this->io->write('Remove installer'); 386 | 387 | // Remove installer script autoloading rules 388 | unset($this->composerDefinition['autoload']['psr-4']['ExpressiveInstaller\\']); 389 | unset($this->composerDefinition['autoload-dev']['psr-4']['ExpressiveInstallerTest\\']); 390 | 391 | // Remove branch-alias 392 | unset($this->composerDefinition['extra']['branch-alias']); 393 | 394 | // Remove installer data 395 | unset($this->composerDefinition['extra']['optional-packages']); 396 | 397 | // Remove installer scripts 398 | unset($this->composerDefinition['scripts']['pre-update-cmd']); 399 | unset($this->composerDefinition['scripts']['pre-install-cmd']); 400 | 401 | // Remove phpstan completely 402 | $this->composerDefinition['scripts']['check'] = array_diff( 403 | $this->composerDefinition['scripts']['check'], 404 | ['@analyze'] 405 | ); 406 | unset($this->composerDefinition['scripts']['analyze']); 407 | } 408 | 409 | /** 410 | * Finalize the package. 411 | * 412 | * Writes the current JSON state to composer.json, clears the 413 | * composer.lock file, and cleans up all files specific to the 414 | * installer. 415 | * 416 | * @codeCoverageIgnore 417 | */ 418 | public function finalizePackage() : void 419 | { 420 | // Update composer definition 421 | $this->composerJson->write($this->composerDefinition); 422 | 423 | $this->clearComposerLockFile(); 424 | $this->cleanUp(); 425 | } 426 | 427 | /** 428 | * Process the answer of a question 429 | * 430 | * @param bool|int|string $answer 431 | */ 432 | public function processAnswer(array $question, $answer) : bool 433 | { 434 | if (is_numeric($answer) && isset($question['options'][$answer])) { 435 | // Add packages to install 436 | if (isset($question['options'][$answer]['packages'])) { 437 | foreach ($question['options'][$answer]['packages'] as $packageName) { 438 | $packageData = $this->config['packages'][$packageName]; 439 | $this->addPackage($packageName, $packageData['version'], $packageData['whitelist'] ?? []); 440 | } 441 | } 442 | 443 | // Copy files 444 | if (isset($question['options'][$answer][$this->installType])) { 445 | $force = ! empty($question['force']); 446 | foreach ($question['options'][$answer][$this->installType] as $resource => $target) { 447 | $this->copyResource($resource, $target, $force); 448 | } 449 | } 450 | 451 | return true; 452 | } 453 | 454 | if ($question['custom-package'] === true && preg_match(self::PACKAGE_REGEX, (string) $answer, $match)) { 455 | $this->addPackage($match['name'], $match['version'], []); 456 | if (isset($question['custom-package-warning'])) { 457 | $this->io->write(sprintf(' %s', $question['custom-package-warning'])); 458 | } 459 | 460 | return true; 461 | } 462 | 463 | return false; 464 | } 465 | 466 | /** 467 | * Add a package 468 | */ 469 | public function addPackage(string $packageName, string $packageVersion, array $whitelist = []) : void 470 | { 471 | $this->io->write(sprintf( 472 | ' - Adding package %s (%s)', 473 | $packageName, 474 | $packageVersion 475 | )); 476 | 477 | // Get the version constraint 478 | $versionParser = new VersionParser(); 479 | $constraint = $versionParser->parseConstraints($packageVersion); 480 | 481 | // Create package link 482 | $link = new Link('__root__', $packageName, $constraint, 'requires', $packageVersion); 483 | 484 | // Add package to the root package and composer.json requirements 485 | if (in_array($packageName, $this->config['require-dev'], true)) { 486 | unset($this->composerDefinition['require'][$packageName]); 487 | unset($this->composerRequires[$packageName]); 488 | 489 | $this->composerDefinition['require-dev'][$packageName] = $packageVersion; 490 | $this->composerDevRequires[$packageName] = $link; 491 | } else { 492 | unset($this->composerDefinition['require-dev'][$packageName]); 493 | unset($this->composerDevRequires[$packageName]); 494 | 495 | $this->composerDefinition['require'][$packageName] = $packageVersion; 496 | $this->composerRequires[$packageName] = $link; 497 | } 498 | 499 | // Set package stability if needed 500 | switch (VersionParser::parseStability($packageVersion)) { 501 | case 'dev': 502 | $this->stabilityFlags[$packageName] = BasePackage::STABILITY_DEV; 503 | break; 504 | case 'alpha': 505 | $this->stabilityFlags[$packageName] = BasePackage::STABILITY_ALPHA; 506 | break; 507 | case 'beta': 508 | $this->stabilityFlags[$packageName] = BasePackage::STABILITY_BETA; 509 | break; 510 | case 'RC': 511 | $this->stabilityFlags[$packageName] = BasePackage::STABILITY_RC; 512 | break; 513 | } 514 | 515 | // Whitelist packages for the component installer 516 | foreach ($whitelist as $package) { 517 | if (! in_array($package, $this->composerDefinition['extra']['zf']['component-whitelist'], true)) { 518 | $this->composerDefinition['extra']['zf']['component-whitelist'][] = $package; 519 | $this->io->write(sprintf(' - Whitelist package %s', $package)); 520 | } 521 | } 522 | } 523 | 524 | /** 525 | * Copy a file to its final destination in the skeleton. 526 | * 527 | * @param string $resource Resource file. 528 | * @param string $target Destination. 529 | * @param bool $force Whether or not to copy over an existing file. 530 | */ 531 | public function copyResource(string $resource, string $target, bool $force = false) : void 532 | { 533 | // Copy file 534 | if ($force === false && is_file($this->projectRoot . $target)) { 535 | return; 536 | } 537 | 538 | $destinationPath = dirname($this->projectRoot . $target); 539 | if (! is_dir($destinationPath)) { 540 | mkdir($destinationPath, 0775, true); 541 | } 542 | 543 | $this->io->write(sprintf(' - Copying %s', $target)); 544 | copy($this->installerSource . $resource, $this->projectRoot . $target); 545 | } 546 | 547 | /** 548 | * Remove lines from string content containing words in array. 549 | */ 550 | public function removeLinesContainingStrings(array $entries, string $content) : string 551 | { 552 | $entries = implode('|', array_map(function ($word) { 553 | return preg_quote($word, '/'); 554 | }, $entries)); 555 | 556 | return preg_replace('/^.*(?:' . $entries . ").*$(?:\r?\n)?/m", '', $content); 557 | } 558 | 559 | /** 560 | * Clean up/remove installer classes and assets. 561 | * 562 | * On completion of install/update, removes the installer classes (including 563 | * this one) and assets (including configuration and templates). 564 | * 565 | * @codeCoverageIgnore 566 | */ 567 | private function cleanUp() : void 568 | { 569 | $this->io->write('Removing Expressive installer classes, configuration, tests and docs'); 570 | foreach ($this->assetsToRemove as $target) { 571 | $target = $this->projectRoot . $target; 572 | if (file_exists($target)) { 573 | unlink($target); 574 | } 575 | } 576 | 577 | $this->recursiveRmdir($this->installerSource); 578 | $this->recursiveRmdir($this->projectRoot . 'test/ExpressiveInstallerTest'); 579 | $this->recursiveRmdir($this->projectRoot . 'docs'); 580 | 581 | $this->preparePhpunitConfig(); 582 | } 583 | 584 | /** 585 | * Remove the ExpressiveInstaller exclusion from the phpunit configuration 586 | * 587 | * @codeCoverageIgnore 588 | */ 589 | private function preparePhpunitConfig() : void 590 | { 591 | $phpunitConfigFile = $this->projectRoot . 'phpunit.xml.dist'; 592 | $phpunitConfig = file_get_contents($phpunitConfigFile); 593 | $phpunitConfig = $this->removeLinesContainingStrings(['exclude', 'ExpressiveInstaller'], $phpunitConfig); 594 | file_put_contents($phpunitConfigFile, $phpunitConfig); 595 | } 596 | 597 | /** 598 | * Prepare and ask questions and return the answer 599 | * 600 | * @param int|string $defaultOption 601 | * @return bool|int|string 602 | * @codeCoverageIgnore 603 | */ 604 | private function askQuestion(array $question, $defaultOption) 605 | { 606 | // Construct question 607 | $ask = [ 608 | sprintf("\n %s\n", $question['question']), 609 | ]; 610 | 611 | $defaultText = $defaultOption; 612 | 613 | foreach ($question['options'] as $key => $option) { 614 | $defaultText = ($key === $defaultOption) ? $option['name'] : $defaultText; 615 | $ask[] = sprintf(" [%d] %s\n", $key, $option['name']); 616 | } 617 | 618 | if ($question['required'] !== true) { 619 | $ask[] = " [n] None of the above\n"; 620 | } 621 | 622 | $ask[] = ($question['custom-package'] === true) 623 | ? sprintf( 624 | ' Make your selection or type a composer package name and version (%s): ', 625 | $defaultText 626 | ) 627 | : sprintf(' Make your selection (%s): ', $defaultText); 628 | 629 | while (true) { 630 | // Ask for user input 631 | $answer = $this->io->ask(implode($ask), (string) $defaultOption); 632 | 633 | // Handle none of the options 634 | if ($answer === 'n' && $question['required'] !== true) { 635 | return 'n'; 636 | } 637 | 638 | // Handle numeric options 639 | if (is_numeric($answer) && isset($question['options'][(int) $answer])) { 640 | return (int) $answer; 641 | } 642 | 643 | // Search for package 644 | if ($question['custom-package'] === true && preg_match(self::PACKAGE_REGEX, $answer, $match)) { 645 | $packageName = $match['name']; 646 | $packageVersion = $match['version']; 647 | 648 | if (! $packageVersion) { 649 | $this->io->write('No package version specified'); 650 | continue; 651 | } 652 | 653 | $this->io->write(sprintf(' - Searching for %s:%s', $packageName, $packageVersion)); 654 | 655 | $optionalPackage = $this->composer->getRepositoryManager()->findPackage($packageName, $packageVersion); 656 | if (null === $optionalPackage) { 657 | $this->io->write(sprintf('Package not found %s:%s', $packageName, $packageVersion)); 658 | continue; 659 | } 660 | 661 | return sprintf('%s:%s', $packageName, $packageVersion); 662 | } 663 | 664 | $this->io->write('Invalid answer'); 665 | } 666 | 667 | return false; 668 | } 669 | 670 | /** 671 | * If a minimal install was requested, remove the default middleware and assets. 672 | * 673 | * @codeCoverageIgnore 674 | */ 675 | private function removeDefaultModule() : void 676 | { 677 | $this->io->write('Removing default App module classes and factories'); 678 | $this->recursiveRmdir($this->projectRoot . '/src/App'); 679 | 680 | $this->io->write('Removing default App module tests'); 681 | $this->recursiveRmdir($this->projectRoot . '/test/AppTest'); 682 | 683 | $this->io->write('Removing App module registration from configuration'); 684 | $this->removeAppModuleConfig(); 685 | } 686 | 687 | /** 688 | * Recursively remove a directory. 689 | * 690 | * @codeCoverageIgnore 691 | */ 692 | private function recursiveRmdir(string $directory) : void 693 | { 694 | if (! is_dir($directory)) { 695 | return; 696 | } 697 | 698 | $rdi = new RecursiveDirectoryIterator($directory, FilesystemIterator::SKIP_DOTS); 699 | $rii = new RecursiveIteratorIterator($rdi, RecursiveIteratorIterator::CHILD_FIRST); 700 | foreach ($rii as $filename => $fileInfo) { 701 | if ($fileInfo->isDir()) { 702 | rmdir($filename); 703 | continue; 704 | } 705 | unlink($filename); 706 | } 707 | rmdir($directory); 708 | } 709 | 710 | /** 711 | * Removes composer.lock file from gitignore. 712 | * 713 | * @codeCoverageIgnore 714 | */ 715 | private function clearComposerLockFile() : void 716 | { 717 | $this->io->write('Removing composer.lock from .gitignore'); 718 | 719 | $ignoreFile = sprintf('%s/.gitignore', $this->projectRoot); 720 | 721 | $content = $this->removeLinesContainingStrings(['composer.lock'], file_get_contents($ignoreFile)); 722 | file_put_contents($ignoreFile, $content); 723 | } 724 | 725 | /** 726 | * Removes the App\ConfigProvider entry from the application config file. 727 | */ 728 | private function removeAppModuleConfig() : void 729 | { 730 | $configFile = $this->projectRoot . '/config/config.php'; 731 | $contents = file_get_contents($configFile); 732 | $contents = str_replace(self::APP_MODULE_CONFIG, '', $contents); 733 | file_put_contents($configFile, $contents); 734 | } 735 | 736 | /** 737 | * Parses the composer file and populates internal data 738 | */ 739 | private function parseComposerDefinition(Composer $composer, string $composerFile) : void 740 | { 741 | $this->composerJson = new JsonFile($composerFile); 742 | $this->composerDefinition = $this->composerJson->read(); 743 | 744 | // Get root package or root alias package 745 | $this->rootPackage = $composer->getPackage(); 746 | 747 | // Get required packages 748 | $this->composerRequires = $this->rootPackage->getRequires(); 749 | $this->composerDevRequires = $this->rootPackage->getDevRequires(); 750 | 751 | // Get stability flags 752 | $this->stabilityFlags = $this->rootPackage->getStabilityFlags(); 753 | } 754 | } 755 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/config/container-aura-di.php: -------------------------------------------------------------------------------- 1 | [ 19 | 'invokables' => [ 20 | ], 21 | 'factories' => [ 22 | ErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class, 23 | 'Zend\Expressive\Whoops' => Container\WhoopsFactory::class, 24 | 'Zend\Expressive\WhoopsPageHandler' => Container\WhoopsPageHandlerFactory::class, 25 | ], 26 | ], 27 | 28 | 'whoops' => [ 29 | 'json_exceptions' => [ 30 | 'display' => true, 31 | 'show_trace' => true, 32 | 'ajax_only' => true, 33 | ], 34 | ], 35 | ]; 36 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/config/routes-full.php: -------------------------------------------------------------------------------- 1 | get('/', App\Handler\HomePageHandler::class, 'home'); 13 | * $app->post('/album', App\Handler\AlbumCreateHandler::class, 'album.create'); 14 | * $app->put('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.put'); 15 | * $app->patch('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.patch'); 16 | * $app->delete('/album/:id', App\Handler\AlbumDeleteHandler::class, 'album.delete'); 17 | * 18 | * Or with multiple request methods: 19 | * 20 | * $app->route('/contact', App\Handler\ContactHandler::class, ['GET', 'POST', ...], 'contact'); 21 | * 22 | * Or handling all request methods: 23 | * 24 | * $app->route('/contact', App\Handler\ContactHandler::class)->setName('contact'); 25 | * 26 | * or: 27 | * 28 | * $app->route( 29 | * '/contact', 30 | * App\Handler\ContactHandler::class, 31 | * Zend\Expressive\Router\Route::HTTP_METHOD_ANY, 32 | * 'contact' 33 | * ); 34 | */ 35 | return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { 36 | $app->get('/', App\Handler\HomePageHandler::class, 'home'); 37 | $app->get('/api/ping', App\Handler\PingHandler::class, 'api.ping'); 38 | }; 39 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/config/routes-minimal.php: -------------------------------------------------------------------------------- 1 | get('/', App\Handler\HomePageHandler::class, 'home'); 13 | * $app->post('/album', App\Handler\AlbumCreateHandler::class, 'album.create'); 14 | * $app->put('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.put'); 15 | * $app->patch('/album/:id', App\Handler\AlbumUpdateHandler::class, 'album.patch'); 16 | * $app->delete('/album/:id', App\Handler\AlbumDeleteHandler::class, 'album.delete'); 17 | * 18 | * Or with multiple request methods: 19 | * 20 | * $app->route('/contact', App\Handler\ContactHandler::class, ['GET', 'POST', ...], 'contact'); 21 | */ 22 | return function (Application $app, MiddlewareFactory $factory, ContainerInterface $container) : void { 23 | }; 24 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/src/ConfigProvider.flat.php: -------------------------------------------------------------------------------- 1 | $this->getDependencies(), 25 | 'templates' => $this->getTemplates(), 26 | ]; 27 | } 28 | 29 | /** 30 | * Returns the container dependencies 31 | */ 32 | public function getDependencies() : array 33 | { 34 | return [ 35 | 'invokables' => [ 36 | Handler\PingHandler::class => Handler\PingHandler::class, 37 | ], 38 | 'factories' => [ 39 | Handler\HomePageHandler::class => Handler\HomePageHandlerFactory::class, 40 | ], 41 | ]; 42 | } 43 | 44 | /** 45 | * Returns the templates configuration 46 | */ 47 | public function getTemplates() : array 48 | { 49 | return [ 50 | 'paths' => [ 51 | 'app' => ['templates/app'], 52 | 'error' => ['templates/error'], 53 | 'layout' => ['templates/layout'], 54 | ], 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/src/ConfigProvider.modular.php: -------------------------------------------------------------------------------- 1 | $this->getDependencies(), 25 | 'templates' => $this->getTemplates(), 26 | ]; 27 | } 28 | 29 | /** 30 | * Returns the container dependencies 31 | */ 32 | public function getDependencies() : array 33 | { 34 | return [ 35 | 'invokables' => [ 36 | Handler\PingHandler::class => Handler\PingHandler::class, 37 | ], 38 | 'factories' => [ 39 | Handler\HomePageHandler::class => Handler\HomePageHandlerFactory::class, 40 | ], 41 | ]; 42 | } 43 | 44 | /** 45 | * Returns the templates configuration 46 | */ 47 | public function getTemplates() : array 48 | { 49 | return [ 50 | 'paths' => [ 51 | 'app' => [__DIR__ . '/../templates/app'], 52 | 'error' => [__DIR__ . '/../templates/error'], 53 | 'layout' => [__DIR__ . '/../templates/layout'], 54 | ], 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/plates/404.phtml: -------------------------------------------------------------------------------- 1 | layout('layout::default', ['title' => '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 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/plates/error.phtml: -------------------------------------------------------------------------------- 1 | layout('layout::default', ['title' => sprintf('%d %s', $status, $reason)]) ?> 2 | 3 |

Oops!

4 |

This is awkward.

5 |

We encountered a e($status)?> e($reason)?> error.

6 | 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 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/plates/home-page.phtml: -------------------------------------------------------------------------------- 1 | layout('layout::default', ['title' => '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. 24 | You decide how you 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 | PSR Container. 67 |

68 | 69 |

70 | 71 | Get started with e($containerName) ?>. 72 | 73 |

74 | 75 |
76 | 77 |
78 |

79 | 80 | Routers 81 | 82 |

83 |

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

87 | 88 |

89 | 90 | Get started with e($routerName) ?>. 91 | 92 |

93 | 94 |
95 | 96 |
97 |

98 | 99 | Templating 100 | 101 |

102 |

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

108 | 109 |

110 | 111 | Get started with e($templateName) ?>. 112 | 113 |

114 | 115 |
116 |
117 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/plates/layout.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <?=$this->e($title)?> - zend-expressive 8 | 9 | 10 | 11 | 17 | section('stylesheets')?> 18 | 19 | 20 |
21 | 54 |
55 | 56 |
57 |
58 | section('content')?> 59 |
60 |
61 | 62 |
63 |
64 |
65 | section('footer')): ?> 66 | section('footer')?> 67 | 68 |

69 | © 2005 - by Zend Technologies Ltd. All rights reserved. 70 |

71 | 72 |
73 |
74 | 75 | 76 | 77 | section('javascript')?> 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/twig/404.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@layout/default.html.twig' %} 2 | 3 | {% block title %}404 Not Found{% endblock %} 4 | 5 | {% block content %} 6 |

Oops!

7 |

This is awkward.

8 |

We encountered a 404 Not Found error.

9 |

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

13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/twig/error.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@layout/default.html.twig' %} 2 | 3 | {% block title %}{{ status }} {{ reason }}{% endblock %} 4 | 5 | {% block content %} 6 |

Oops!

7 |

This is awkward.

8 |

We encountered a {{ status }} {{ reason }} error.

9 | {% if status == 404 %} 10 |

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

14 | {% endif %} 15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/twig/home-page.html.twig: -------------------------------------------------------------------------------- 1 | {% extends '@layout/default.html.twig' %} 2 | 3 | {% block title %}Home{% endblock %} 4 | 5 | {% block content %} 6 |
7 |

Welcome to zend-expressive

8 |

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

13 |

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

16 |
17 | 18 |
19 |
20 |

21 | 22 | Agile & Lean 23 | 24 |

25 |

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

29 |
30 | 31 |
32 |

33 | 34 | HTTP Messages 35 | 36 |

37 |

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

42 |
43 | 44 |
45 |

46 | 47 | Middleware 48 | 49 |

50 |

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

56 |
57 |
58 | 59 |
60 |
61 |

62 | 63 | Containers 64 | 65 |

66 |

67 | Expressive promotes and advocates the usage of Dependency Injection/Inversion of Control containers 68 | when writing your applications. Expressive supports multiple containers which typehints against 69 | PSR Container. 70 |

71 | {% if containerName is defined %} 72 |

73 | 74 | Get started with {{ containerName }}. 75 | 76 |

77 | {% endif %} 78 |
79 | 80 |
81 |

82 | 83 | Routers 84 | 85 |

86 |

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

90 | {% if routerName is defined %} 91 |

92 | 93 | Get started with {{ routerName }}. 94 | 95 |

96 | {% endif %} 97 |
98 | 99 |
100 |

101 | 102 | Templating 103 | 104 |

105 |

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

111 | {% if templateName is defined %} 112 |

113 | 114 | Get started with {{ templateName }}. 115 | 116 |

117 | {% endif %} 118 |
119 |
120 | {% endblock %} 121 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/twig/layout.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} - zend-expressive 8 | 9 | 10 | 11 | 17 | {% block stylesheets %}{% endblock %} 18 | 19 | 20 |
21 | 54 |
55 | 56 |
57 |
58 | {% block content %}{% endblock %} 59 |
60 |
61 | 62 |
63 |
64 |
65 | {% block footer %} 66 |

67 | © 2005 - {{ "now"|date("Y") }} by Zend Technologies Ltd. All rights reserved. 68 |

69 | {% endblock %} 70 |
71 |
72 | 73 | 74 | 75 | {% block javascript %}{% endblock %} 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/zend-view/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 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/zend-view/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 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/zend-view/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. 24 | You decide how you 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 | PSR Container. 67 |

68 | containerName)) : ?> 69 |

70 | 71 | Get started with containerName ?>. 72 | 73 |

74 | 75 |
76 | 77 |
78 |

79 | 80 | Routers 81 | 82 |

83 |

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

87 | routerName)) : ?> 88 |

89 | 90 | Get started with routerName ?>. 91 | 92 |

93 | 94 |
95 | 96 |
97 |

98 | 99 | Templating 100 | 101 |

102 |

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

108 | templateName)) : ?> 109 |

110 | 111 | Get started with templateName ?>. 112 | 113 |

114 | 115 |
116 |
117 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/Resources/templates/zend-view/layout.phtml: -------------------------------------------------------------------------------- 1 | headLink() 3 | ->prependStylesheet('https://use.fontawesome.com/releases/v5.0.6/css/all.css') 4 | ->prependStylesheet('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css'); 5 | $this->inlineScript() 6 | ->prependFile('https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js') 7 | ->prependFile('https://code.jquery.com/jquery-3.3.1.min.js'); 8 | ?> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | headTitle('zend-expressive')->setSeparator(' - ')->setAutoEscape(false)?> 17 | headMeta()?> 18 | headLink()?> 19 | 25 | 26 | 27 |
28 | 61 |
62 | 63 |
64 |
65 | content?> 66 |
67 |
68 | 69 |
70 |
71 |
72 |

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

75 |
76 |
77 | 78 | inlineScript()?> 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/ExpressiveInstaller/config.php: -------------------------------------------------------------------------------- 1 | [ 7 | 'elie29/zend-phpdi-config' => [ 8 | 'version' => '^3.0', 9 | ], 10 | 'filp/whoops' => [ 11 | 'version' => '^2.1.12', 12 | ], 13 | 'jsoumelidis/zend-sf-di-config' => [ 14 | 'version' => '^0.3', 15 | ], 16 | 'northwoods/container' => [ 17 | 'version' => '^3.0', 18 | ], 19 | 'zendframework/zend-auradi-config' => [ 20 | 'version' => '^1.0', 21 | ], 22 | 'zendframework/zend-expressive-aurarouter' => [ 23 | 'version' => '^3.0', 24 | 'whitelist' => [ 25 | 'zendframework/zend-expressive-aurarouter' 26 | ] 27 | ], 28 | 'zendframework/zend-expressive-fastroute' => [ 29 | 'version' => '^3.0', 30 | 'whitelist' => [ 31 | 'zendframework/zend-expressive-fastroute' 32 | ] 33 | ], 34 | 'zendframework/zend-expressive-platesrenderer' => [ 35 | 'version' => '^2.0', 36 | 'whitelist' => [ 37 | 'zendframework/zend-expressive-platesrenderer' 38 | ] 39 | ], 40 | 'zendframework/zend-expressive-twigrenderer' => [ 41 | 'version' => '^2.0', 42 | 'whitelist' => [ 43 | 'zendframework/zend-expressive-twigrenderer' 44 | ] 45 | ], 46 | 'zendframework/zend-expressive-zendrouter' => [ 47 | 'version' => '^3.0', 48 | 'whitelist' => [ 49 | 'zendframework/zend-expressive-zendrouter' 50 | ] 51 | ], 52 | 'zendframework/zend-expressive-zendviewrenderer' => [ 53 | 'version' => '^2.0', 54 | 'whitelist' => [ 55 | 'zendframework/zend-expressive-zendviewrenderer' 56 | ] 57 | ], 58 | 'zendframework/zend-pimple-config' => [ 59 | 'version' => '^1.0', 60 | ], 61 | 'zendframework/zend-servicemanager' => [ 62 | 'version' => '^3.3', 63 | ], 64 | ], 65 | 66 | 'require-dev' => [ 67 | 'filp/whoops', 68 | ], 69 | 70 | 'application' => [ 71 | 'flat' => [ 72 | 'packages' => [], 73 | 'resources' => [ 74 | 'Resources/src/ConfigProvider.flat.php' => 'src/App/ConfigProvider.php', 75 | ], 76 | ], 77 | 'modular' => [ 78 | 'packages' => [ 79 | ], 80 | 'resources' => [ 81 | 'Resources/src/ConfigProvider.modular.php' => 'src/App/src/ConfigProvider.php', 82 | ], 83 | ], 84 | ], 85 | 86 | 'questions' => [ 87 | 'container' => [ 88 | 'question' => 'Which container do you want to use for dependency injection?', 89 | 'default' => 3, 90 | 'required' => true, 91 | 'custom-package' => true, 92 | 'custom-package-warning' => 'You need to edit public/index.php to start the custom container.', 93 | 'options' => [ 94 | 1 => [ 95 | 'name' => 'Aura.Di', 96 | 'packages' => [ 97 | 'zendframework/zend-auradi-config', 98 | ], 99 | 'flat' => [ 100 | 'Resources/config/container-aura-di.php' => 'config/container.php', 101 | ], 102 | 'modular' => [ 103 | 'Resources/config/container-aura-di.php' => 'config/container.php', 104 | ], 105 | 'minimal' => [ 106 | 'Resources/config/container-aura-di.php' => 'config/container.php', 107 | ], 108 | ], 109 | 2 => [ 110 | 'name' => 'Pimple', 111 | 'packages' => [ 112 | 'zendframework/zend-pimple-config', 113 | ], 114 | 'flat' => [ 115 | 'Resources/config/container-pimple.php' => 'config/container.php', 116 | ], 117 | 'modular' => [ 118 | 'Resources/config/container-pimple.php' => 'config/container.php', 119 | ], 120 | 'minimal' => [ 121 | 'Resources/config/container-pimple.php' => 'config/container.php', 122 | ], 123 | ], 124 | 3 => [ 125 | 'name' => 'zend-servicemanager', 126 | 'packages' => [ 127 | 'zendframework/zend-servicemanager', 128 | ], 129 | 'flat' => [ 130 | 'Resources/config/container-zend-servicemanager.php' => 'config/container.php', 131 | ], 132 | 'modular' => [ 133 | 'Resources/config/container-zend-servicemanager.php' => 'config/container.php', 134 | ], 135 | 'minimal' => [ 136 | 'Resources/config/container-zend-servicemanager.php' => 'config/container.php', 137 | ], 138 | ], 139 | 4 => [ 140 | 'name' => 'Auryn', 141 | 'packages' => [ 142 | 'northwoods/container', 143 | ], 144 | 'flat' => [ 145 | 'Resources/config/container-auryn.php' => 'config/container.php', 146 | ], 147 | 'modular' => [ 148 | 'Resources/config/container-auryn.php' => 'config/container.php', 149 | ], 150 | 'minimal' => [ 151 | 'Resources/config/container-auryn.php' => 'config/container.php', 152 | ], 153 | ], 154 | 5 => [ 155 | 'name' => 'Symfony DI Container', 156 | 'packages' => [ 157 | 'jsoumelidis/zend-sf-di-config', 158 | ], 159 | 'flat' => [ 160 | 'Resources/config/container-sf-di.php' => 'config/container.php', 161 | ], 162 | 'modular' => [ 163 | 'Resources/config/container-sf-di.php' => 'config/container.php', 164 | ], 165 | 'minimal' => [ 166 | 'Resources/config/container-sf-di.php' => 'config/container.php', 167 | ], 168 | ], 169 | 6 => [ 170 | 'name' => 'PHP-DI', 171 | 'packages' => [ 172 | 'elie29/zend-phpdi-config', 173 | ], 174 | 'flat' => [ 175 | 'Resources/config/container-php-di.php' => 'config/container.php', 176 | ], 177 | 'modular' => [ 178 | 'Resources/config/container-php-di.php' => 'config/container.php', 179 | ], 180 | 'minimal' => [ 181 | 'Resources/config/container-php-di.php' => 'config/container.php', 182 | ], 183 | ], 184 | ], 185 | ], 186 | 187 | 'router' => [ 188 | 'question' => 'Which router do you want to use?', 189 | 'default' => 2, 190 | // TRUE: Must choose one / FALSE: May choose one or none of the above 191 | 'required' => true, 192 | // Enable custom package input 193 | 'custom-package' => true, 194 | // Display warning when choosing a custom package 195 | 'custom-package-warning' => 'You need to write your own router adapter.', 196 | 'options' => [ 197 | 1 => [ 198 | 'name' => 'Aura.Router', 199 | 'packages' => [ 200 | 'zendframework/zend-expressive-aurarouter', 201 | ], 202 | 'flat' => [ 203 | 'Resources/config/routes-full.php' => 'config/routes.php', 204 | ], 205 | 'modular' => [ 206 | 'Resources/config/routes-full.php' => 'config/routes.php', 207 | ], 208 | 'minimal' => [ 209 | 'Resources/config/routes-minimal.php' => 'config/routes.php', 210 | ], 211 | ], 212 | 2 => [ 213 | 'name' => 'FastRoute', 214 | 'packages' => [ 215 | 'zendframework/zend-expressive-fastroute', 216 | ], 217 | 'flat' => [ 218 | 'Resources/config/routes-full.php' => 'config/routes.php', 219 | ], 220 | 'modular' => [ 221 | 'Resources/config/routes-full.php' => 'config/routes.php', 222 | ], 223 | 'minimal' => [ 224 | 'Resources/config/routes-minimal.php' => 'config/routes.php', 225 | ], 226 | ], 227 | 3 => [ 228 | 'name' => 'zend-router', 229 | 'packages' => [ 230 | 'zendframework/zend-expressive-zendrouter', 231 | ], 232 | 'flat' => [ 233 | 'Resources/config/routes-full.php' => 'config/routes.php', 234 | ], 235 | 'modular' => [ 236 | 'Resources/config/routes-full.php' => 'config/routes.php', 237 | ], 238 | 'minimal' => [ 239 | 'Resources/config/routes-minimal.php' => 'config/routes.php', 240 | ], 241 | ], 242 | ], 243 | ], 244 | 245 | 'template-engine' => [ 246 | 'question' => 'Which template engine do you want to use?', 247 | 'default' => 'n', 248 | 'required' => false, 249 | 'custom-package' => true, 250 | 'options' => [ 251 | 1 => [ 252 | 'name' => 'Plates', 253 | 'packages' => [ 254 | 'zendframework/zend-expressive-platesrenderer', 255 | ], 256 | 'flat' => [ 257 | 'Resources/templates/plates/404.phtml' => 'templates/error/404.phtml', 258 | 'Resources/templates/plates/error.phtml' => 'templates/error/error.phtml', 259 | 'Resources/templates/plates/layout.phtml' => 'templates/layout/default.phtml', 260 | 'Resources/templates/plates/home-page.phtml' => 'templates/app/home-page.phtml', 261 | ], 262 | 'modular' => [ 263 | 'Resources/templates/plates/404.phtml' => 'src/App/templates/error/404.phtml', 264 | 'Resources/templates/plates/error.phtml' => 'src/App/templates/error/error.phtml', 265 | 'Resources/templates/plates/layout.phtml' => 'src/App/templates/layout/default.phtml', 266 | 'Resources/templates/plates/home-page.phtml' => 'src/App/templates/app/home-page.phtml', 267 | ], 268 | 'minimal' => [ 269 | ], 270 | ], 271 | 2 => [ 272 | 'name' => 'Twig', 273 | 'packages' => [ 274 | 'zendframework/zend-expressive-twigrenderer', 275 | ], 276 | 'flat' => [ 277 | 'Resources/templates/twig/404.html.twig' => 'templates/error/404.html.twig', 278 | 'Resources/templates/twig/error.html.twig' => 'templates/error/error.html.twig', 279 | 'Resources/templates/twig/layout.html.twig' => 'templates/layout/default.html.twig', 280 | 'Resources/templates/twig/home-page.html.twig' => 'templates/app/home-page.html.twig', 281 | ], 282 | 'modular' => [ 283 | 'Resources/templates/twig/404.html.twig' => 'src/App/templates/error/404.html.twig', 284 | 'Resources/templates/twig/error.html.twig' => 'src/App/templates/error/error.html.twig', 285 | 'Resources/templates/twig/layout.html.twig' => 'src/App/templates/layout/default.html.twig', 286 | 'Resources/templates/twig/home-page.html.twig' => 'src/App/templates/app/home-page.html.twig', 287 | ], 288 | 'minimal' => [ 289 | ], 290 | ], 291 | 3 => [ 292 | 'name' => 'zend-view installs zend-servicemanager', 293 | 'packages' => [ 294 | 'zendframework/zend-expressive-zendviewrenderer', 295 | ], 296 | 'flat' => [ 297 | 'Resources/templates/zend-view/404.phtml' => 'templates/error/404.phtml', 298 | 'Resources/templates/zend-view/error.phtml' => 'templates/error/error.phtml', 299 | 'Resources/templates/zend-view/layout.phtml' => 'templates/layout/default.phtml', 300 | 'Resources/templates/zend-view/home-page.phtml' => 'templates/app/home-page.phtml', 301 | ], 302 | 'modular' => [ 303 | 'Resources/templates/zend-view/404.phtml' => 'src/App/templates/error/404.phtml', 304 | 'Resources/templates/zend-view/error.phtml' => 'src/App/templates/error/error.phtml', 305 | 'Resources/templates/zend-view/layout.phtml' => 'src/App/templates/layout/default.phtml', 306 | 'Resources/templates/zend-view/home-page.phtml' => 'src/App/templates/app/home-page.phtml', 307 | ], 308 | 'minimal' => [ 309 | ], 310 | ], 311 | ], 312 | ], 313 | 314 | 'error-handler' => [ 315 | 'question' => 'Which error handler do you want to use during development?', 316 | 'default' => 1, 317 | 'required' => false, 318 | 'custom-package' => true, 319 | 'force' => true, 320 | 'options' => [ 321 | 1 => [ 322 | 'name' => 'Whoops', 323 | 'packages' => [ 324 | 'filp/whoops', 325 | ], 326 | 'flat' => [ 327 | 'Resources/config/error-handler-whoops.php' => 'config/autoload/development.local.php.dist', 328 | ], 329 | 'modular' => [ 330 | 'Resources/config/error-handler-whoops.php' => 'config/autoload/development.local.php.dist', 331 | ], 332 | 'minimal' => [ 333 | 'Resources/config/error-handler-whoops.php' => 'config/autoload/development.local.php.dist', 334 | ], 335 | ], 336 | ], 337 | ], 338 | ], 339 | ]; 340 | -------------------------------------------------------------------------------- /test/AppTest/Handler/HomePageHandlerFactoryTest.php: -------------------------------------------------------------------------------- 1 | container = $this->prophesize(ContainerInterface::class); 23 | $router = $this->prophesize(RouterInterface::class); 24 | 25 | $this->container->get(RouterInterface::class)->willReturn($router); 26 | } 27 | 28 | public function testFactoryWithoutTemplate() 29 | { 30 | $factory = new HomePageHandlerFactory(); 31 | $this->container->has(TemplateRendererInterface::class)->willReturn(false); 32 | 33 | $this->assertInstanceOf(HomePageHandlerFactory::class, $factory); 34 | 35 | $homePage = $factory($this->container->reveal()); 36 | 37 | $this->assertInstanceOf(HomePageHandler::class, $homePage); 38 | } 39 | 40 | public function testFactoryWithTemplate() 41 | { 42 | $this->container->has(TemplateRendererInterface::class)->willReturn(true); 43 | $this->container 44 | ->get(TemplateRendererInterface::class) 45 | ->willReturn($this->prophesize(TemplateRendererInterface::class)); 46 | 47 | $factory = new HomePageHandlerFactory(); 48 | 49 | $homePage = $factory($this->container->reveal()); 50 | 51 | $this->assertInstanceOf(HomePageHandler::class, $homePage); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/AppTest/Handler/HomePageHandlerTest.php: -------------------------------------------------------------------------------- 1 | container = $this->prophesize(ContainerInterface::class); 29 | $this->router = $this->prophesize(RouterInterface::class); 30 | } 31 | 32 | public function testReturnsJsonResponseWhenNoTemplateRendererProvided() 33 | { 34 | $homePage = new HomePageHandler( 35 | get_class($this->container->reveal()), 36 | $this->router->reveal(), 37 | null 38 | ); 39 | $response = $homePage->handle( 40 | $this->prophesize(ServerRequestInterface::class)->reveal() 41 | ); 42 | 43 | $this->assertInstanceOf(JsonResponse::class, $response); 44 | } 45 | 46 | public function testReturnsHtmlResponseWhenTemplateRendererProvided() 47 | { 48 | $renderer = $this->prophesize(TemplateRendererInterface::class); 49 | $renderer 50 | ->render('app::home-page', Argument::type('array')) 51 | ->willReturn(''); 52 | 53 | $homePage = new HomePageHandler( 54 | get_class($this->container->reveal()), 55 | $this->router->reveal(), 56 | $renderer->reveal() 57 | ); 58 | 59 | $response = $homePage->handle( 60 | $this->prophesize(ServerRequestInterface::class)->reveal() 61 | ); 62 | 63 | $this->assertInstanceOf(HtmlResponse::class, $response); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/AppTest/Handler/PingHandlerTest.php: -------------------------------------------------------------------------------- 1 | handle( 18 | $this->prophesize(ServerRequestInterface::class)->reveal() 19 | ); 20 | 21 | $json = json_decode((string) $response->getBody()); 22 | 23 | $this->assertInstanceOf(JsonResponse::class, $response); 24 | $this->assertTrue(isset($json->ack)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/AddPackageTest.php: -------------------------------------------------------------------------------- 1 | createOptionalPackages(); 24 | 25 | $this->io 26 | ->write(Argument::containingString('Removing installer development dependencies')) 27 | ->shouldBeCalled(); 28 | $installer->removeDevDependencies(); 29 | 30 | $this->io 31 | ->write(Argument::containingString('Adding package')) 32 | ->shouldBeCalled(); 33 | 34 | $installer->addPackage($packageName, $packageVersion); 35 | 36 | $this->assertPackage('zendframework/zend-stdlib', $installer); 37 | 38 | $r = new ReflectionProperty($installer, 'stabilityFlags'); 39 | $r->setAccessible(true); 40 | $stabilityFlags = $r->getValue($installer); 41 | 42 | // Stability flags are only set for non-stable packages 43 | if ($expectedStability) { 44 | $this->assertArrayHasKey($packageName, $stabilityFlags); 45 | $this->assertEquals($expectedStability, $stabilityFlags[$packageName]); 46 | } 47 | } 48 | 49 | public function packageProvider() : array 50 | { 51 | // $packageName, $packageVersion, $expectedStability 52 | return [ 53 | 'dev' => ['zendframework/zend-stdlib', '1.0.0-dev', BasePackage::STABILITY_DEV], 54 | 'alpha' => ['zendframework/zend-stdlib', '1.0.0-alpha2', BasePackage::STABILITY_ALPHA], 55 | 'beta' => ['zendframework/zend-stdlib', '1.0.0-beta.3', BasePackage::STABILITY_BETA], 56 | 'RC' => ['zendframework/zend-stdlib', '1.0.0-RC4', BasePackage::STABILITY_RC], 57 | 'stable' => ['zendframework/zend-stdlib', '1.0.0', null], 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/ContainersTest.php: -------------------------------------------------------------------------------- 1 | projectRoot = $this->copyProjectFilesToTempFilesystem(); 35 | $this->installer = $this->createOptionalPackages($this->projectRoot); 36 | } 37 | 38 | protected function tearDown() 39 | { 40 | parent::tearDown(); 41 | chdir($this->packageRoot); 42 | $this->recursiveDelete($this->projectRoot); 43 | $this->tearDownAlternateAutoloader(); 44 | } 45 | 46 | /** 47 | * @runInSeparateProcess 48 | * 49 | * @dataProvider containerProvider 50 | */ 51 | public function testContainer( 52 | string $installType, 53 | int $containerOption, 54 | int $routerOption, 55 | string $copyFilesKey, 56 | int $expectedResponseStatusCode, 57 | string $expectedContainer 58 | ) { 59 | $this->prepareSandboxForInstallType($installType, $this->installer); 60 | 61 | // Install container 62 | $config = $this->getInstallerConfig($this->installer); 63 | $containerResult = $this->installer->processAnswer( 64 | $config['questions']['container'], 65 | $containerOption 66 | ); 67 | $this->assertTrue($containerResult); 68 | 69 | // Install router 70 | $routerResult = $this->installer->processAnswer( 71 | $config['questions']['router'], 72 | $routerOption 73 | ); 74 | $this->assertTrue($routerResult); 75 | 76 | $configFile = $this->projectRoot . DIRECTORY_SEPARATOR . '/config/config.php'; 77 | $configFileContents = file_get_contents($configFile); 78 | $configFileContents = preg_replace( 79 | '/(new ConfigAggregator\(\[)/s', 80 | '$1' . "\n \Zend\Expressive\\Router\\FastRouteRouter\ConfigProvider::class,\n", 81 | $configFileContents 82 | ); 83 | file_put_contents($configFile, $configFileContents); 84 | 85 | // Test container 86 | $container = $this->getContainer(); 87 | $this->assertInstanceOf(ContainerInterface::class, $container); 88 | $this->assertInstanceOf($expectedContainer, $container); 89 | $this->assertTrue($container->has(Expressive\Helper\UrlHelper::class)); 90 | $this->assertTrue($container->has(Expressive\Helper\ServerUrlHelper::class)); 91 | $this->assertTrue($container->has(Expressive\Application::class)); 92 | $this->assertTrue($container->has(Expressive\Router\RouterInterface::class)); 93 | 94 | // Test home page 95 | $setupRoutes = strpos($copyFilesKey, 'minimal') !== 0; 96 | $response = $this->getAppResponse('/', $setupRoutes); 97 | $this->assertEquals($expectedResponseStatusCode, $response->getStatusCode()); 98 | } 99 | 100 | public function containerProvider() : array 101 | { 102 | // @codingStandardsIgnoreStart 103 | // $installType, $containerOption, $routerOption, $copyFilesKey, $expectedResponseStatusCode, $expectedContainer 104 | return [ 105 | 'aura-minimal' => [OptionalPackages::INSTALL_MINIMAL, 1, 2, 'minimal-files', 404, AuraContainer::class], 106 | 'aura-flat' => [OptionalPackages::INSTALL_FLAT, 1, 2, 'copy-files', 200, AuraContainer::class], 107 | 'aura-modular' => [OptionalPackages::INSTALL_MODULAR, 1, 2, 'copy-files', 200, AuraContainer::class], 108 | 'pimple-minimal' => [OptionalPackages::INSTALL_MINIMAL, 2, 2, 'minimal-files', 404, PimpleContainer::class], 109 | 'pimple-flat' => [OptionalPackages::INSTALL_FLAT, 2, 2, 'copy-files', 200, PimpleContainer::class], 110 | 'pimple-modular' => [OptionalPackages::INSTALL_MODULAR, 2, 2, 'copy-files', 200, PimpleContainer::class], 111 | 'zend-sm-minimal' => [OptionalPackages::INSTALL_MINIMAL, 3, 2, 'minimal-files', 404, ZendServiceManagerContainer::class], 112 | 'zend-sm-flat' => [OptionalPackages::INSTALL_FLAT, 3, 2, 'copy-files', 200, ZendServiceManagerContainer::class], 113 | 'zend-sm-modular' => [OptionalPackages::INSTALL_MODULAR, 3, 2, 'copy-files', 200, ZendServiceManagerContainer::class], 114 | 'auryn-minimal' => [OptionalPackages::INSTALL_MINIMAL, 4, 2, 'minimal-files', 404, AurynContainer::class], 115 | 'auryn-flat' => [OptionalPackages::INSTALL_FLAT, 4, 2, 'copy-files', 200, AurynContainer::class], 116 | 'auryn-modular' => [OptionalPackages::INSTALL_MODULAR, 4, 2, 'copy-files', 200, AurynContainer::class], 117 | 'sf-di-minimal' => [OptionalPackages::INSTALL_MINIMAL, 5, 2, 'minimal-files', 404, SfContainerBuilder::class], 118 | 'sf-di-flat' => [OptionalPackages::INSTALL_FLAT, 5, 2, 'copy-files', 200, SfContainerBuilder::class], 119 | 'sf-di-modular' => [OptionalPackages::INSTALL_MODULAR, 5, 2, 'copy-files', 200, SfContainerBuilder::class], 120 | 'php-di-minimal' => [OptionalPackages::INSTALL_MINIMAL, 6, 2, 'minimal-files', 404, PhpDIContainer::class], 121 | 'php-di-flat' => [OptionalPackages::INSTALL_FLAT, 6, 2, 'copy-files', 200, PhpDIContainer::class], 122 | 'php-di-modular' => [OptionalPackages::INSTALL_MODULAR, 6, 2, 'copy-files', 200, PhpDIContainer::class], 123 | ]; 124 | // @codingStandardsIgnoreEnd 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/CopyResourceTest.php: -------------------------------------------------------------------------------- 1 | project = vfsStream::setup('project-root'); 31 | $this->projectRoot = vfsStream::url('project-root'); 32 | } 33 | 34 | public function testTargetFileDoesNotExist() 35 | { 36 | $this->assertFalse($this->project->hasChild('data')); 37 | $this->assertFalse($this->project->hasChild('data/target.php')); 38 | } 39 | 40 | public function testResourceIsCopiedIfItDoesNotExist() 41 | { 42 | $installer = $this->createOptionalPackages($this->projectRoot); 43 | 44 | $installer->copyResource('config.php', 'data/target.php'); 45 | 46 | $this->assertTrue($this->project->hasChild('data')); 47 | $this->assertTrue($this->project->hasChild('data/target.php')); 48 | $this->assertFileEquals( 49 | $this->packageRoot . '/src/ExpressiveInstaller/config.php', 50 | vfsStream::url('project-root/data/target.php') 51 | ); 52 | } 53 | 54 | public function testResourceIsNotCopiedIfItExists() 55 | { 56 | // Create default test file 57 | vfsStream::newFile('data/target.php') 58 | ->at($this->project) 59 | ->setContent('TEST'); 60 | 61 | $this->assertTrue($this->project->hasChild('data/target.php')); 62 | 63 | // Copy file (should not copy file) 64 | $installer = $this->createOptionalPackages($this->projectRoot); 65 | $installer->copyResource('config.php', 'data/target.php'); 66 | 67 | $this->assertTrue($this->project->hasChild('data/target.php')); 68 | $this->assertEquals( 69 | 'TEST', 70 | file_get_contents(vfsStream::url('project-root/data/target.php')) 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/ErrorHandlerTest.php: -------------------------------------------------------------------------------- 1 | projectRoot = $this->copyProjectFilesToTempFilesystem(); 29 | $this->installer = $this->createOptionalPackages($this->projectRoot); 30 | } 31 | 32 | protected function tearDown() 33 | { 34 | parent::tearDown(); 35 | chdir($this->packageRoot); 36 | $this->recursiveDelete($this->projectRoot); 37 | $this->tearDownAlternateAutoloader(); 38 | } 39 | 40 | /** 41 | * @runInSeparateProcess 42 | */ 43 | public function testErrorHandlerIsNotInstalled() 44 | { 45 | $this->prepareSandboxForInstallType(OptionalPackages::INSTALL_MINIMAL, $this->installer); 46 | 47 | // Install container 48 | $config = $this->getInstallerConfig($this->installer); 49 | $containerResult = $this->installer->processAnswer( 50 | $config['questions']['container'], 51 | 3 52 | ); 53 | $this->assertTrue($containerResult); 54 | 55 | // Enable development mode 56 | $this->enableDevelopmentMode(); 57 | 58 | // Test container 59 | $container = $this->getContainer(); 60 | $this->assertTrue($container->has(ErrorResponseGenerator::class)); 61 | $this->assertFalse($container->has('Zend\Expressive\Whoops')); 62 | $this->assertFalse($container->has('Zend\Expressive\WhoopsPageHandler')); 63 | } 64 | 65 | /** 66 | * @runInSeparateProcess 67 | * 68 | * @dataProvider errorHandlerProvider 69 | */ 70 | public function testErrorHandler( 71 | string $installType, 72 | int $containerOption, 73 | int $errorHandlerOption, 74 | string $expectedErrorHandler 75 | ) { 76 | $this->prepareSandboxForInstallType($installType, $this->installer); 77 | $config = $this->getInstallerConfig($this->installer); 78 | 79 | // Install container 80 | $containerResult = $this->installer->processAnswer( 81 | $config['questions']['container'], 82 | $containerOption 83 | ); 84 | $this->assertTrue($containerResult); 85 | 86 | // Install error handler 87 | $containerResult = $this->installer->processAnswer( 88 | $config['questions']['error-handler'], 89 | $errorHandlerOption 90 | ); 91 | $this->assertTrue($containerResult); 92 | 93 | // Enable development mode 94 | $this->enableDevelopmentMode(); 95 | 96 | // Test container 97 | $container = $this->getContainer(); 98 | $this->assertTrue($container->has(ErrorResponseGenerator::class)); 99 | $this->assertTrue($container->has('Zend\Expressive\Whoops')); 100 | $this->assertTrue($container->has('Zend\Expressive\WhoopsPageHandler')); 101 | 102 | $config = $container->get('config'); 103 | $this->assertEquals( 104 | $expectedErrorHandler, 105 | $config['dependencies']['factories'][ErrorResponseGenerator::class] 106 | ); 107 | } 108 | 109 | public function errorHandlerProvider() : array 110 | { 111 | // $installType, $containerOption, $errorHandlerOption, $expectedErrorHandler 112 | return [ 113 | 'whoops-minimal' => [OptionalPackages::INSTALL_MINIMAL, 3, 1, WhoopsErrorResponseGeneratorFactory::class], 114 | 'whoops-full' => [OptionalPackages::INSTALL_FLAT, 3, 1, WhoopsErrorResponseGeneratorFactory::class], 115 | 'whoops-modular' => [OptionalPackages::INSTALL_MODULAR, 3, 1, WhoopsErrorResponseGeneratorFactory::class], 116 | ]; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/OptionalPackagesTestCase.php: -------------------------------------------------------------------------------- 1 | setAccessible(true); 63 | if (array_key_exists($package, $r->getValue($installer))) { 64 | $found = true; 65 | break; 66 | } 67 | } 68 | 69 | self::assertThat($found, self::isTrue(), $message); 70 | } 71 | 72 | /** 73 | * Assert that the installer DOES NOT contain a specification for the package. 74 | * 75 | * @throws AssertionFailedError 76 | */ 77 | public static function assertNotPackage(string $package, OptionalPackages $installer, string $message = null) 78 | { 79 | $message = $message ?: sprintf('Failed asserting that package "%s" is absent from the installer', $package); 80 | $found = false; 81 | 82 | foreach (['composerRequires', 'composerDevRequires'] as $property) { 83 | $r = new ReflectionProperty($installer, $property); 84 | $r->setAccessible(true); 85 | if (array_key_exists($package, $r->getValue($installer))) { 86 | $found = true; 87 | break; 88 | } 89 | } 90 | 91 | self::assertThat($found, self::isFalse(), $message); 92 | } 93 | 94 | /** 95 | * Assert that the installer DOES NOT contain a specification for each package in the list. 96 | * 97 | * @param string[] $packages 98 | * @throws AssertionFailedError 99 | */ 100 | public static function assertPackages(array $packages, OptionalPackages $installer, string $message = null) 101 | { 102 | foreach ($packages as $package) { 103 | self::assertPackage($package, $installer, $message); 104 | } 105 | } 106 | 107 | /** 108 | * Assert that the installer contains a specification for each package in the list. 109 | * 110 | * @param string[] $packages 111 | * @throws AssertionFailedError 112 | */ 113 | public static function assertNotPackages(array $packages, OptionalPackages $installer, string $message = null) 114 | { 115 | foreach ($packages as $package) { 116 | self::assertNotPackage($package, $installer, $message); 117 | } 118 | } 119 | 120 | /** 121 | * Assert that the installer contains a specification for the package. 122 | * 123 | * @throws AssertionFailedError 124 | */ 125 | public static function assertWhitelisted(string $package, OptionalPackages $installer, string $message = null) 126 | { 127 | $message = $message ?: sprintf('Failed asserting that package "%s" is whitelisted in composer.json', $package); 128 | $found = false; 129 | 130 | $r = new ReflectionProperty($installer, 'composerDefinition'); 131 | $r->setAccessible(true); 132 | $whitelist = $r->getValue($installer)['extra']['zf']['component-whitelist']; 133 | 134 | if (in_array($package, $whitelist)) { 135 | $found = true; 136 | } 137 | 138 | self::assertThat($found, self::isTrue(), $message); 139 | } 140 | 141 | protected function setUp() 142 | { 143 | $this->packageRoot = realpath(__DIR__ . '/../../'); 144 | putenv('COMPOSER=' . $this->packageRoot . '/composer.json'); 145 | } 146 | 147 | protected function tearDown() 148 | { 149 | putenv('COMPOSER='); 150 | } 151 | 152 | /** 153 | * Create the OptionalPackages installer instance. 154 | * 155 | * Creates the IOInterface and Composer mock instances when doing so, 156 | * and uses the provided $projectRoot, if specified. 157 | */ 158 | protected function createOptionalPackages(string $projectRoot = null) : OptionalPackages 159 | { 160 | $projectRoot = $projectRoot ?: $this->packageRoot; 161 | 162 | $this->io = $this->prophesize(IOInterface::class); 163 | return new OptionalPackages( 164 | $this->io->reveal(), 165 | $this->createComposer()->reveal(), 166 | $projectRoot 167 | ); 168 | } 169 | 170 | protected function createComposer() 171 | { 172 | $this->composer = $composer = $this->prophesize(Composer::class); 173 | $composer->getPackage()->will([$this->createRootPackage(), 'reveal']); 174 | return $composer; 175 | } 176 | 177 | protected function createRootPackage() 178 | { 179 | $composerJson = file_get_contents($this->packageRoot . '/composer.json'); 180 | $this->composerJson = $composer = json_decode($composerJson, true); 181 | $this->rootPackage = $package = $this->prophesize(RootPackage::class); 182 | 183 | $package->getRequires()->willReturn($composer['require']); 184 | $package->getDevRequires()->willReturn($composer['require-dev']); 185 | $package->getStabilityFlags()->willReturn($this->getStabilityFlags()); 186 | 187 | return $package; 188 | } 189 | 190 | protected function getStabilityFlags() 191 | { 192 | $r = new ReflectionClass(OptionalPackages::class); 193 | $properties = $r->getDefaultProperties(); 194 | return array_fill_keys($properties['devDependencies'], BasePackage::STABILITY_DEV); 195 | } 196 | 197 | /** 198 | * Retrieve a single property value from the installer. 199 | * 200 | * @return mixed 201 | */ 202 | protected function getInstallerProperty(OptionalPackages $installer, string $property) 203 | { 204 | $r = new ReflectionProperty($installer, $property); 205 | $r->setAccessible(true); 206 | return $r->getValue($installer); 207 | } 208 | 209 | /** 210 | * Retrieve the stored composer data structure from an installer instance. 211 | */ 212 | protected function getComposerDataFromInstaller(OptionalPackages $installer) : array 213 | { 214 | return $this->getInstallerProperty($installer, 'composerDefinition'); 215 | } 216 | 217 | /** 218 | * Retrieve the stored resource configuration from an installer instance. 219 | */ 220 | protected function getInstallerConfig(OptionalPackages $installer) : array 221 | { 222 | return $this->getInstallerProperty($installer, 'config'); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/ProcessAnswersTest.php: -------------------------------------------------------------------------------- 1 | projectRoot = $this->copyProjectFilesToTempFilesystem(); 28 | $this->installer = $this->createOptionalPackages($this->projectRoot); 29 | $this->prepareSandboxForInstallType(OptionalPackages::INSTALL_MINIMAL, $this->installer); 30 | } 31 | 32 | protected function tearDown() 33 | { 34 | parent::tearDown(); 35 | chdir($this->packageRoot); 36 | $this->recursiveDelete($this->projectRoot); 37 | } 38 | 39 | public function testInvalidAnswer() 40 | { 41 | $this->io->write()->shouldNotBeCalled(); 42 | 43 | $config = $this->getInstallerConfig($this->installer); 44 | $question = $config['questions']['container']; 45 | $answer = 'foobar'; 46 | $result = $this->installer->processAnswer($question, $answer); 47 | 48 | $this->assertFalse($result); 49 | $this->assertFileNotExists($this->projectRoot . '/config/container.php'); 50 | } 51 | 52 | public function testAnsweredWithN() 53 | { 54 | $this->io->write()->shouldNotBeCalled(); 55 | 56 | $config = $this->getInstallerConfig($this->installer); 57 | $question = $config['questions']['container']; 58 | $answer = 'n'; 59 | $result = $this->installer->processAnswer($question, $answer); 60 | 61 | $this->assertFalse($result); 62 | $this->assertFileNotExists($this->projectRoot . '/config/container.php'); 63 | } 64 | 65 | public function testAnsweredWithInvalidOption() 66 | { 67 | $this->io->write()->shouldNotBeCalled(); 68 | 69 | $config = $this->getInstallerConfig($this->installer); 70 | $question = $config['questions']['container']; 71 | $answer = 10; 72 | $result = $this->installer->processAnswer($question, $answer); 73 | 74 | $this->assertFalse($result); 75 | $this->assertFileNotExists($this->projectRoot . '/config/container.php'); 76 | } 77 | 78 | public function testAnsweredWithValidOption() 79 | { 80 | // Prepare the installer 81 | $this->installer->removeDevDependencies(); 82 | 83 | // @codingStandardsIgnoreStart 84 | $this->io->write(Argument::containingString('Adding package zendframework/zend-auradi-config'))->shouldBeCalled(); 85 | $this->io->write(Argument::containingString('Copying config/container.php'))->shouldBeCalled(); 86 | // @codingStandardsIgnoreEnd 87 | 88 | $config = $this->getInstallerConfig($this->installer); 89 | $question = $config['questions']['container']; 90 | $answer = 1; 91 | $result = $this->installer->processAnswer($question, $answer); 92 | 93 | $this->assertTrue($result); 94 | $this->assertFileExists($this->projectRoot . '/config/container.php'); 95 | $this->assertPackage('zendframework/zend-auradi-config', $this->installer); 96 | } 97 | 98 | public function testAnsweredWithPackage() 99 | { 100 | // Prepare the installer 101 | $this->installer->removeDevDependencies(); 102 | 103 | $this->io->write(Argument::containingString('Adding package league/container'))->shouldBeCalled(); 104 | $this->io->write(Argument::containingString('You need to edit public/index.php'))->shouldBeCalled(); 105 | 106 | $config = $this->getInstallerConfig($this->installer); 107 | $question = $config['questions']['container']; 108 | $answer = 'league/container:2.2.0'; 109 | $result = $this->installer->processAnswer($question, $answer); 110 | 111 | $this->assertTrue($result); 112 | $this->assertFileNotExists($this->projectRoot . '/config/container.php'); 113 | $this->assertPackage('league/container', $this->installer); 114 | } 115 | 116 | public function testPackagesAreAddedToWhitelist() 117 | { 118 | // Prepare the installer 119 | $this->installer->removeDevDependencies(); 120 | 121 | $this->io 122 | ->write(Argument::containingString( 123 | 'Adding package zendframework/zend-expressive-zendviewrenderer' 124 | )) 125 | ->shouldBeCalled(); 126 | $this->io 127 | ->write(Argument::containingString( 128 | 'Whitelist package zendframework/zend-expressive-zendviewrenderer' 129 | )) 130 | ->shouldBeCalled(); 131 | 132 | $config = $this->getInstallerConfig($this->installer); 133 | $question = $config['questions']['template-engine']; 134 | $answer = 3; 135 | $result = $this->installer->processAnswer($question, $answer); 136 | 137 | $this->assertTrue($result); 138 | $this->assertPackage('zendframework/zend-expressive-zendviewrenderer', $this->installer); 139 | $this->assertWhitelisted('zendframework/zend-expressive-zendviewrenderer', $this->installer); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/ProjectSandboxTrait.php: -------------------------------------------------------------------------------- 1 | projectRoot = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('exp'); 61 | 62 | mkdir($this->projectRoot . '/data', 0777, true); 63 | mkdir($this->projectRoot . '/data/cache', 0777, true); 64 | foreach (['config', 'public', 'src', 'templates', 'test'] as $path) { 65 | $this->recursiveCopy( 66 | $this->packageRoot . DIRECTORY_SEPARATOR . $path, 67 | $this->projectRoot . DIRECTORY_SEPARATOR . $path 68 | ); 69 | } 70 | 71 | chdir($this->projectRoot); 72 | return $this->projectRoot; 73 | } 74 | 75 | /** 76 | * Prepare the sandbox for the selected instalation type. 77 | * 78 | * Sets the installer's install type, and sets up the application structure. 79 | * 80 | * If a non-minimal install type is selected, also sets up the alternate 81 | * autoloader to ensure the `App` namespace resolves correctly. 82 | * 83 | * @param string $installType 84 | * @param OptionalPackages $installer 85 | */ 86 | protected function prepareSandboxForInstallType($installType, OptionalPackages $installer) 87 | { 88 | $installer->setInstallType($installType); 89 | $installer->setupDefaultApp($installType); 90 | 91 | switch ($installType) { 92 | case OptionalPackages::INSTALL_FLAT: 93 | $this->setUpAlternateAutoloader('/src/'); 94 | break; 95 | case OptionalPackages::INSTALL_MODULAR: 96 | $this->setUpAlternateAutoloader('/src/App/src/', true); 97 | break; 98 | } 99 | } 100 | 101 | /** 102 | * Enable development-mode configuration within the sandbox. 103 | */ 104 | protected function enableDevelopmentMode() 105 | { 106 | $target = sprintf( 107 | '%s%sconfig%sdevelopment.config.php', 108 | $this->projectRoot, 109 | DIRECTORY_SEPARATOR, 110 | DIRECTORY_SEPARATOR 111 | ); 112 | copy($target . '.dist', $target); 113 | 114 | Assert::assertFileExists($target); 115 | 116 | $target = sprintf( 117 | '%s%sconfig%sautoload%sdevelopment.local.php', 118 | $this->projectRoot, 119 | DIRECTORY_SEPARATOR, 120 | DIRECTORY_SEPARATOR, 121 | DIRECTORY_SEPARATOR 122 | ); 123 | copy($target . '.dist', $target); 124 | 125 | Assert::assertFileExists($target); 126 | } 127 | 128 | /** 129 | * Adds an alternate autoloader to the stack for the App namespace. 130 | * 131 | * Required, as the tests will load classes from that namespace, but the 132 | * class files will exist in temporary directories. 133 | * 134 | * Any test that uses this (and it's implicit when using setInstallType()) 135 | * MUST run in a separate process. 136 | * 137 | * tearDown() unregisters the autoloader. 138 | * 139 | * @param string $appPath The path to the App namespace source code, 140 | * relative to the project root. 141 | * @param bool $stripNamespace Whether or not to strip the initial 142 | * namespace when determining the path (ala PSR-4). 143 | */ 144 | protected function setUpAlternateAutoloader(string $appPath, bool $stripNamespace = false) 145 | { 146 | $this->autoloader = function ($class) use ($appPath, $stripNamespace) { 147 | if (0 !== strpos($class, 'App\\')) { 148 | return false; 149 | } 150 | 151 | $class = $stripNamespace 152 | ? str_replace('App\\', '', $class) 153 | : $class; 154 | 155 | $path = $this->projectRoot 156 | . $appPath 157 | . str_replace('\\', '/', $class) 158 | . '.php'; 159 | 160 | if (! file_exists($path)) { 161 | return false; 162 | } 163 | 164 | include $path; 165 | }; 166 | 167 | spl_autoload_register($this->autoloader, true, true); 168 | } 169 | 170 | /** 171 | * Remove the alternate autolader, if present. 172 | */ 173 | protected function tearDownAlternateAutoloader() 174 | { 175 | if ($this->autoloader) { 176 | spl_autoload_unregister($this->autoloader); 177 | unset($this->autoloader); 178 | } 179 | } 180 | 181 | /** 182 | * Returns the configured container for the sandbox project. 183 | * 184 | * @return ContainerInterface; 185 | */ 186 | protected function getContainer() 187 | { 188 | if ($this->container) { 189 | return $this->container; 190 | } 191 | 192 | $path = $this->projectRoot 193 | ? $this->projectRoot . '/config/container.php' 194 | : 'config/container.php'; 195 | 196 | /** @var ContainerInterface $container */ 197 | $this->container = require $path; 198 | 199 | return $this->container; 200 | } 201 | 202 | /** 203 | * Creates and dispatches an application at the requested path. 204 | * 205 | * @param string $path Path to request within the application 206 | * @param bool $setupRoutes Whether or not to setup routes before dispatch 207 | */ 208 | protected function getAppResponse(string $path = '/', bool $setupRoutes = true) : ResponseInterface 209 | { 210 | $container = $this->getContainer(); 211 | 212 | /** @var Application $app */ 213 | $app = $container->get(Application::class); 214 | 215 | // Import programmatic/declarative middleware pipeline and routing configuration statements 216 | $app->pipe(ErrorHandler::class); 217 | $app->pipe(ServerUrlMiddleware::class); 218 | $app->pipe(RouteMiddleware::class); 219 | $app->pipe(MethodNotAllowedMiddleware::class); 220 | $app->pipe(ImplicitHeadMiddleware::class); 221 | $app->pipe(ImplicitOptionsMiddleware::class); 222 | $app->pipe(UrlHelperMiddleware::class); 223 | $app->pipe(DispatchMiddleware::class); 224 | $app->pipe(NotFoundHandler::class); 225 | 226 | if ($setupRoutes === true && $container->has(HomePageHandler::class)) { 227 | $app->get('/', HomePageHandler::class, 'home'); 228 | } 229 | 230 | if ($setupRoutes === true && $container->has(PingHandler::class)) { 231 | $app->get('/api/ping', PingHandler::class, 'api.ping'); 232 | } 233 | 234 | return $app->handle( 235 | new ServerRequest([], [], 'https://example.com' . $path, 'GET') 236 | ); 237 | } 238 | 239 | /** 240 | * Recursively copy the files from one tree to another. 241 | */ 242 | protected function recursiveCopy(string $source, string $target) 243 | { 244 | if (! is_dir($target)) { 245 | mkdir($target, 0777, true); 246 | } 247 | 248 | if (! is_dir($source)) { 249 | return; 250 | } 251 | 252 | $dir = new DirectoryIterator($source); 253 | foreach ($dir as $fileInfo) { 254 | if ($fileInfo->isFile()) { 255 | $realPath = $fileInfo->getRealPath(); 256 | $path = ltrim(str_replace($source, '', $realPath), '/\\'); 257 | copy($realPath, sprintf('%s/%s', $target, $path)); 258 | continue; 259 | } 260 | 261 | if ($fileInfo->isDir() && ! $fileInfo->isDot()) { 262 | $path = $fileInfo->getFilename(); 263 | 264 | mkdir($target . '/' . $path, 0777, true); 265 | 266 | $this->recursiveCopy( 267 | $source . DIRECTORY_SEPARATOR . $path, 268 | $target . DIRECTORY_SEPARATOR . $path 269 | ); 270 | continue; 271 | } 272 | } 273 | } 274 | 275 | /** 276 | * Recursively remove a filesystem tree. 277 | * 278 | * @param string $target Tree to remove. 279 | */ 280 | protected function recursiveDelete(string $target) 281 | { 282 | if (! is_dir($target)) { 283 | return; 284 | } 285 | 286 | foreach (scandir($target) as $node) { 287 | if (in_array($node, ['.', '..'], true)) { 288 | continue; 289 | } 290 | 291 | $path = sprintf('%s/%s', $target, $node); 292 | 293 | if (is_dir($path)) { 294 | $this->recursiveDelete($path); 295 | continue; 296 | } 297 | 298 | unlink($path); 299 | } 300 | 301 | rmdir($target); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/PromptForOptionalPackagesTest.php: -------------------------------------------------------------------------------- 1 | projectRoot = $this->copyProjectFilesToTempFilesystem(); 29 | 30 | // This test suite writes to the composer.json, so we need to use a copy. 31 | copy($this->packageRoot . '/composer.json', $this->projectRoot . '/composer.json'); 32 | putenv('COMPOSER=' . $this->projectRoot . '/composer.json'); 33 | 34 | $this->installer = $this->createOptionalPackages($this->projectRoot); 35 | $this->prepareSandboxForInstallType(OptionalPackages::INSTALL_MINIMAL, $this->installer); 36 | } 37 | 38 | protected function tearDown() 39 | { 40 | parent::tearDown(); 41 | chdir($this->packageRoot); 42 | $this->recursiveDelete($this->projectRoot); 43 | } 44 | 45 | public function promptCombinations() 46 | { 47 | $config = require __DIR__ . '/../../src/ExpressiveInstaller/config.php'; 48 | foreach ($config['questions'] as $questionName => $question) { 49 | foreach ($question['options'] as $selection => $package) { 50 | $name = sprintf('%s-%s', $questionName, $package['name']); 51 | yield $name => [$questionName, $question, $selection, $package]; 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * @dataProvider promptCombinations 58 | */ 59 | public function testPromptForOptionalPackage( 60 | string $questionName, 61 | array $question, 62 | int $selection, 63 | array $expectedPackage 64 | ) { 65 | $this->io 66 | ->ask( 67 | Argument::that(function ($arg) use ($question) { 68 | PromptForOptionalPackagesTest::assertPromptText($question['question'], $arg); 69 | 70 | return true; 71 | }), 72 | $question['default'] 73 | ) 74 | ->willReturn($selection); 75 | 76 | foreach ($expectedPackage['packages'] as $package) { 77 | $this->io 78 | ->write(Argument::containingString($package)) 79 | ->shouldBeCalled(); 80 | } 81 | 82 | foreach ($expectedPackage[OptionalPackages::INSTALL_MINIMAL] as $target) { 83 | $this->io 84 | ->write(Argument::containingString($target)) 85 | ->shouldBeCalled(); 86 | } 87 | 88 | $this->assertNull($this->installer->promptForOptionalPackage($questionName, $question)); 89 | } 90 | 91 | public static function assertPromptText($expected, $argument) 92 | { 93 | self::assertInternalType( 94 | 'string', 95 | $argument, 96 | 'Questions must be a string since symfony/console:4.0' 97 | ); 98 | 99 | self::assertThat( 100 | false !== strpos($argument, $expected), 101 | self::isTrue(), 102 | sprintf('Expected prompt not received: "%s"', $expected) 103 | ); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/RemoveDevDependenciesTest.php: -------------------------------------------------------------------------------- 1 | getDefaultProperties(); 48 | $this->devDependencies = $props['devDependencies']; 49 | 50 | $this->installer = $this->createOptionalPackages(); 51 | } 52 | 53 | public function testComposerHasAllDependencies() 54 | { 55 | $this->assertPackages($this->standardDependencies, $this->installer); 56 | $this->assertPackages($this->devDependencies, $this->installer); 57 | } 58 | 59 | public function testDevDependenciesAreRemoved() 60 | { 61 | // Remove development dependencies 62 | $this->installer->removeDevDependencies(); 63 | 64 | $this->assertPackages($this->standardDependencies, $this->installer); 65 | $this->assertNotPackages($this->devDependencies, $this->installer); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/RemoveInstallerTest.php: -------------------------------------------------------------------------------- 1 | installer = $this->createOptionalPackages(); 25 | } 26 | 27 | public function testComposerHasInstaller() 28 | { 29 | $composer = $this->getComposerDataFromInstaller($this->installer); 30 | 31 | $this->assertTrue(isset($composer['autoload']['psr-4']['ExpressiveInstaller\\'])); 32 | $this->assertTrue(isset($composer['autoload-dev']['psr-4']['ExpressiveInstallerTest\\'])); 33 | $this->assertTrue(isset($composer['extra']['branch-alias'])); 34 | $this->assertFalse(isset($composer['extra']['optional-packages'])); 35 | $this->assertTrue(isset($composer['scripts']['pre-install-cmd'])); 36 | $this->assertTrue(isset($composer['scripts']['pre-update-cmd'])); 37 | } 38 | 39 | public function testInstallerIsRemoved() 40 | { 41 | // Remove the installer 42 | $this->installer->removeInstallerFromDefinition(); 43 | 44 | $composer = $this->getComposerDataFromInstaller($this->installer); 45 | 46 | $this->assertFalse(isset($composer['autoload']['psr-4']['ExpressiveInstaller\\'])); 47 | $this->assertFalse(isset($composer['autoload-dev']['psr-4']['ExpressiveInstallerTest\\'])); 48 | $this->assertFalse(isset($composer['extra']['branch-alias'])); 49 | $this->assertFalse(isset($composer['extra']['optional-packages'])); 50 | $this->assertFalse(isset($composer['scripts']['pre-install-cmd'])); 51 | $this->assertFalse(isset($composer['scripts']['pre-update-cmd'])); 52 | $this->assertFalse(isset($composer['scripts']['check']['@analyze'])); 53 | $this->assertFalse(isset($composer['scripts']['analyze'])); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/RemoveLineFromStringTest.php: -------------------------------------------------------------------------------- 1 | installer = $this->createOptionalPackages(); 25 | } 26 | 27 | public function testRemoveFirstLine() 28 | { 29 | $string = "foo\nbar\nbaz\nqux\nquux"; 30 | 31 | $actual = $this->installer->removeLinesContainingStrings(['foo'], $string); 32 | $expected = "bar\nbaz\nqux\nquux"; 33 | 34 | $this->assertEquals($expected, $actual); 35 | } 36 | 37 | public function testRemoveSingleLine() 38 | { 39 | $string = "foo\nbar\nbaz\nqux\nquux"; 40 | 41 | $actual = $this->installer->removeLinesContainingStrings(['bar'], $string); 42 | $expected = "foo\nbaz\nqux\nquux"; 43 | 44 | $this->assertEquals($expected, $actual); 45 | } 46 | 47 | public function testRemoveMultipleLines() 48 | { 49 | $string = "foo\nbar\nbaz\nqux\nquux"; 50 | 51 | $actual = $this->installer->removeLinesContainingStrings(['bar', 'baz'], $string); 52 | $expected = "foo\nqux\nquux"; 53 | 54 | $this->assertEquals($expected, $actual); 55 | } 56 | 57 | public function testRemoveLinesWithSpaces() 58 | { 59 | $string = "foo\n bar\n baz \n qux\nquux"; 60 | 61 | $actual = $this->installer->removeLinesContainingStrings(['bar', 'baz'], $string); 62 | $expected = "foo\n qux\nquux"; 63 | 64 | $this->assertEquals($expected, $actual); 65 | } 66 | 67 | public function testRemoveLastLine() 68 | { 69 | $string = "foo\nbar\nbaz\nqux\nquux"; 70 | 71 | $actual = $this->installer->removeLinesContainingStrings(['quux'], $string); 72 | $expected = "foo\nbar\nbaz\nqux\n"; 73 | 74 | $this->assertEquals($expected, $actual); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/RequestInstallTypeTest.php: -------------------------------------------------------------------------------- 1 | installer = $this->createOptionalPackages(); 26 | } 27 | 28 | public function installSelections() : array 29 | { 30 | return [ 31 | OptionalPackages::INSTALL_MINIMAL => ['1', OptionalPackages::INSTALL_MINIMAL], 32 | OptionalPackages::INSTALL_FLAT => ['2', OptionalPackages::INSTALL_FLAT], 33 | OptionalPackages::INSTALL_MODULAR => ['3', OptionalPackages::INSTALL_MODULAR], 34 | ]; 35 | } 36 | 37 | /** 38 | * @dataProvider installSelections 39 | */ 40 | public function testRequestInstallTypeReturnsExpectedConstantValue(string $selection, string $expected) 41 | { 42 | $this->io 43 | ->ask(Argument::that([__CLASS__, 'assertQueryPrompt']), '2') 44 | ->willReturn($selection); 45 | 46 | $this->assertSame($expected, $this->installer->requestInstallType()); 47 | } 48 | 49 | public function testWillContinueToPromptUntilValidAnswerPresented() 50 | { 51 | $io = $this->io; 52 | $tries = mt_rand(1, 10); 53 | 54 | // Handle a call to ask() by looping $tries times 55 | $handle = function () use ($io, &$tries, &$handle) { 56 | if (0 === $tries) { 57 | // Valid choice to complete the loop 58 | return '1'; 59 | } 60 | 61 | // Otherwise, ask again. 62 | $tries -= 1; 63 | $io->ask(Argument::that([__CLASS__, 'assertQueryPrompt']), '2')->will($handle); 64 | return 'n'; 65 | }; 66 | 67 | $this->io 68 | ->ask(Argument::that([__CLASS__, 'assertQueryPrompt']), '2') 69 | ->will($handle); 70 | 71 | $this->io 72 | ->write(Argument::containingString('Invalid answer')) 73 | ->shouldBeCalledTimes($tries); 74 | 75 | $this->assertSame(OptionalPackages::INSTALL_MINIMAL, $this->installer->requestInstallType()); 76 | $this->assertEquals(0, $tries); 77 | } 78 | 79 | public static function assertQueryPrompt($value) 80 | { 81 | self::assertInternalType( 82 | 'string', 83 | $value, 84 | 'Questions must be a string since symfony/console:4.0' 85 | ); 86 | 87 | self::assertThat( 88 | false !== strpos($value, 'What type of installation would you like?'), 89 | self::isTrue(), 90 | 'Unexpected prompt value' 91 | ); 92 | 93 | return true; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/RoutersTest.php: -------------------------------------------------------------------------------- 1 | 'home', 28 | 'path' => '/', 29 | 'middleware' => HomePageHandler::class, 30 | 'allowed_methods' => ['GET'], 31 | ], 32 | [ 33 | 'name' => 'api.ping', 34 | 'path' => '/api/ping', 35 | 'middleware' => PingHandler::class, 36 | 'allowed_methods' => ['GET'], 37 | ], 38 | ]; 39 | 40 | /** 41 | * @var OptionalPackages 42 | */ 43 | private $installer; 44 | 45 | /** 46 | * @var string[] 47 | */ 48 | private $routerConfigProviders = [ 49 | Router\AuraRouter::class => Router\AuraRouter\ConfigProvider::class, 50 | Router\FastRouteRouter::class => Router\FastRouteRouter\ConfigProvider::class, 51 | Router\ZendRouter::class => Router\ZendRouter\ConfigProvider::class, 52 | ]; 53 | 54 | protected function setUp() 55 | { 56 | parent::setUp(); 57 | $this->projectRoot = $this->copyProjectFilesToTempFilesystem(); 58 | $this->installer = $this->createOptionalPackages($this->projectRoot); 59 | } 60 | 61 | protected function tearDown() 62 | { 63 | parent::tearDown(); 64 | chdir($this->packageRoot); 65 | $this->recursiveDelete($this->projectRoot); 66 | $this->tearDownAlternateAutoloader(); 67 | } 68 | 69 | /** 70 | * @runInSeparateProcess 71 | * 72 | * @dataProvider routerProvider 73 | */ 74 | public function testRouter( 75 | string $installType, 76 | int $containerOption, 77 | int $routerOption, 78 | string $copyFilesKey, 79 | string $dependencyKey, 80 | int $expectedResponseStatusCode, 81 | array $expectedRoutes, 82 | string $expectedRouter 83 | ) { 84 | $this->prepareSandboxForInstallType($installType, $this->installer); 85 | 86 | // Install container 87 | $config = $this->getInstallerConfig($this->installer); 88 | $containerResult = $this->installer->processAnswer( 89 | $config['questions']['container'], 90 | $containerOption 91 | ); 92 | $this->assertTrue($containerResult); 93 | 94 | // Install router 95 | $routerResult = $this->installer->processAnswer( 96 | $config['questions']['router'], 97 | $routerOption 98 | ); 99 | $this->assertTrue($routerResult); 100 | $this->enableRouter($expectedRouter); 101 | 102 | // Test container 103 | $container = $this->getContainer(); 104 | $this->assertTrue($container->has(Router\RouterInterface::class)); 105 | 106 | // Test config 107 | $config = $container->get('config'); 108 | $this->assertEquals( 109 | $expectedRouter, 110 | $config['dependencies'][$dependencyKey][Router\RouterInterface::class] 111 | ); 112 | 113 | // Test home page 114 | $setupRoutes = strpos($copyFilesKey, 'minimal') !== 0; 115 | $response = $this->getAppResponse('/', $setupRoutes); 116 | $this->assertEquals($expectedResponseStatusCode, $response->getStatusCode()); 117 | 118 | /** @var Application $app */ 119 | $app = $container->get(Application::class); 120 | $this->assertCount(count($expectedRoutes), $app->getRoutes()); 121 | foreach ($app->getRoutes() as $route) { 122 | foreach ($expectedRoutes as $expectedRoute) { 123 | if ($expectedRoute['name'] === $route->getName()) { 124 | $this->assertEquals($expectedRoute['path'], $route->getPath()); 125 | $this->assertEquals($expectedRoute['allowed_methods'], $route->getAllowedMethods()); 126 | 127 | continue 2; 128 | } 129 | } 130 | 131 | $this->fail(sprintf('Route with name "%s" has not been found', $route->getName())); 132 | } 133 | } 134 | 135 | public function routerProvider() : array 136 | { 137 | // @codingStandardsIgnoreStart 138 | // $containerOption, $routerOption, $copyFilesKey, $dependencyKey, $expectedResponseStatusCode, $expectedRoutes, $expectedRouter 139 | return [ 140 | 'aura-minimal' => [OptionalPackages::INSTALL_MINIMAL, 3, 1, 'minimal-files', 'aliases', 404, [], Router\AuraRouter::class], 141 | 'aura-flat' => [OptionalPackages::INSTALL_FLAT, 3, 1, 'copy-files', 'aliases', 200, $this->expectedRoutes, Router\AuraRouter::class], 142 | 'aura-modular' => [OptionalPackages::INSTALL_MODULAR, 3, 1, 'copy-files', 'aliases', 200, $this->expectedRoutes, Router\AuraRouter::class], 143 | 'fastroute-minimal' => [OptionalPackages::INSTALL_MINIMAL, 3, 2, 'minimal-files', 'aliases', 404, [], Router\FastRouteRouter::class], 144 | 'fastroute-flat' => [OptionalPackages::INSTALL_FLAT, 3, 2, 'copy-files', 'aliases', 200, $this->expectedRoutes, Router\FastRouteRouter::class], 145 | 'fastroute-modular' => [OptionalPackages::INSTALL_MODULAR, 3, 2, 'copy-files', 'aliases', 200, $this->expectedRoutes, Router\FastRouteRouter::class], 146 | 'zend-router-minimal' => [OptionalPackages::INSTALL_MINIMAL, 3, 3, 'minimal-files', 'aliases', 404, [], Router\ZendRouter::class], 147 | 'zend-router-flat' => [OptionalPackages::INSTALL_FLAT, 3, 3, 'copy-files', 'aliases', 200, $this->expectedRoutes, Router\ZendRouter::class], 148 | 'zend-router-modular' => [OptionalPackages::INSTALL_MODULAR, 3, 3, 'copy-files', 'aliases', 200, $this->expectedRoutes, Router\ZendRouter::class], 149 | ]; 150 | // @codingStandardsIgnoreEnd 151 | } 152 | 153 | public function enableRouter(string $expectedRouter) 154 | { 155 | $configFile = $this->projectRoot . '/config/config.php'; 156 | $contents = file_get_contents($configFile); 157 | $contents = preg_replace( 158 | '/(new ConfigAggregator\(\[)/s', 159 | '$1' . "\n " . $this->routerConfigProviders[$expectedRouter] . "::class,\n", 160 | $contents 161 | ); 162 | file_put_contents($configFile, $contents); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/SetupDataAndCacheDirTest.php: -------------------------------------------------------------------------------- 1 | project = vfsStream::setup('project-root'); 38 | $this->projectRoot = vfsStream::url('project-root'); 39 | $this->installer = $this->createOptionalPackages($this->projectRoot); 40 | } 41 | 42 | public function testCreatesDataDirectoryWhenInvoked() 43 | { 44 | $this->io->write(Argument::containingString('Setup data and cache dir'))->shouldBeCalled(); 45 | $this->installer->setupDataAndCacheDir(); 46 | 47 | // '40755' is the octal representation of the file permissions 48 | $this->assertTrue(is_dir($this->projectRoot . '/data'), 'Data directory was not created?'); 49 | $this->assertSame( 50 | '40775', 51 | sprintf('%o', fileperms($this->projectRoot . '/data')), 52 | 'Data directory permissions incorrect' 53 | ); 54 | 55 | $this->assertTrue(is_dir($this->projectRoot . '/data/cache'), 'Cache directory was not created?'); 56 | $this->assertSame( 57 | '40775', 58 | sprintf('%o', fileperms($this->projectRoot . '/data/cache')), 59 | 'Cache directory permissions incorrect' 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/SetupDefaultAppTest.php: -------------------------------------------------------------------------------- 1 | projectRoot = $this->copyProjectFilesToTempFilesystem(); 27 | $this->installer = $this->createOptionalPackages($this->projectRoot); 28 | } 29 | 30 | protected function tearDown() 31 | { 32 | parent::tearDown(); 33 | chdir($this->packageRoot); 34 | $this->recursiveDelete($this->projectRoot); 35 | $this->tearDownAlternateAutoloader(); 36 | } 37 | 38 | public function testModularInstallationAddsToolingSupportAsDevRequirement() 39 | { 40 | $this->prepareSandboxForInstallType(OptionalPackages::INSTALL_MODULAR, $this->installer); 41 | $this->assertPackage( 42 | 'zendframework/zend-expressive-tooling', 43 | $this->installer, 44 | 'zend-expressive-tooling package was not injected into composer.json' 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/TemplateRenderersTest.php: -------------------------------------------------------------------------------- 1 | Expressive\Plates\ConfigProvider::class, 28 | Expressive\Twig\TwigRenderer::class => Expressive\Twig\ConfigProvider::class, 29 | Expressive\ZendView\ZendViewRenderer::class => Expressive\ZendView\ConfigProvider::class, 30 | ]; 31 | 32 | protected function setUp() 33 | { 34 | parent::setUp(); 35 | $this->projectRoot = $this->copyProjectFilesToTempFilesystem(); 36 | $this->installer = $this->createOptionalPackages($this->projectRoot); 37 | } 38 | 39 | protected function tearDown() 40 | { 41 | parent::tearDown(); 42 | chdir($this->packageRoot); 43 | $this->recursiveDelete($this->projectRoot); 44 | $this->tearDownAlternateAutoloader(); 45 | } 46 | 47 | /** 48 | * @runInSeparateProcess 49 | * 50 | * @dataProvider templateRendererProvider 51 | */ 52 | public function testTemplateRenderer( 53 | string $installType, 54 | int $containerOption, 55 | int $routerOption, 56 | int $templateRendererOption, 57 | int $expectedResponseStatusCode, 58 | string $expectedTemplateRenderer 59 | ) { 60 | $this->prepareSandboxForInstallType($installType, $this->installer); 61 | 62 | // Install container 63 | $config = $this->getInstallerConfig($this->installer); 64 | $containerResult = $this->installer->processAnswer( 65 | $config['questions']['container'], 66 | $containerOption 67 | ); 68 | $this->assertTrue($containerResult); 69 | 70 | // Install router 71 | $routerResult = $this->installer->processAnswer( 72 | $config['questions']['router'], 73 | $routerOption 74 | ); 75 | $this->assertTrue($routerResult); 76 | $this->injectRouterConfigProvider(); 77 | 78 | // Install template engine 79 | $templateEngineResult = $this->installer->processAnswer( 80 | $config['questions']['template-engine'], 81 | $templateRendererOption 82 | ); 83 | $this->assertTrue($templateEngineResult); 84 | $this->injectConfigProvider($expectedTemplateRenderer); 85 | 86 | // Test container 87 | $container = $this->getContainer(); 88 | $this->assertTrue($container->has(Expressive\Application::class)); 89 | $this->assertTrue($container->has(Middleware\ErrorHandler::class)); 90 | $this->assertTrue($container->has(Expressive\Template\TemplateRendererInterface::class)); 91 | 92 | // Test config 93 | $config = $container->get('config'); 94 | $this->assertEquals( 95 | Expressive\Container\ErrorHandlerFactory::class, 96 | $config['dependencies']['factories'][Middleware\ErrorHandler::class] 97 | ); 98 | 99 | // Test template renderer 100 | $templateRenderer = $container->get(Expressive\Template\TemplateRendererInterface::class); 101 | $this->assertInstanceOf(Expressive\Template\TemplateRendererInterface::class, $templateRenderer); 102 | $this->assertInstanceOf($expectedTemplateRenderer, $templateRenderer); 103 | 104 | if ($installType !== OptionalPackages::INSTALL_MINIMAL) { 105 | // Test home page for non-minimal installs only, otherwise you get 106 | // invalid template name errors 107 | $response = $this->getAppResponse(); 108 | $this->assertEquals($expectedResponseStatusCode, $response->getStatusCode()); 109 | } 110 | } 111 | 112 | public function templateRendererProvider() : Generator 113 | { 114 | // @codingStandardsIgnoreStart 115 | // Minimal framework installation test cases; no templates installed. 116 | // Must be run before those that install templates and test the output. 117 | // $installType, $containerOption, $routerOption, $templateRendererOption, $expectedResponseStatusCode, $expectedTemplateRenderer 118 | yield 'plates-minimal' => [OptionalPackages::INSTALL_MINIMAL, 3, 2, 1, 404, Expressive\Plates\PlatesRenderer::class]; 119 | yield 'twig-minimal' => [OptionalPackages::INSTALL_MINIMAL, 3, 2, 2, 404, Expressive\Twig\TwigRenderer::class]; 120 | yield 'zend-view-minimal' => [OptionalPackages::INSTALL_MINIMAL, 3, 2, 3, 404, Expressive\ZendView\ZendViewRenderer::class]; 121 | // @codingStandardsIgnoreEnd 122 | 123 | // @codingStandardsIgnoreStart 124 | // Full framework installation test cases; installation options that install templates. 125 | $testCases = [ 126 | // $containerOption, $routerOption, $templateRendererOption, $expectedResponseStatusCode, $expectedTemplateRenderer 127 | 'plates-full' => [3, 2, 1, 200, Expressive\Plates\PlatesRenderer::class], 128 | 'twig-full' => [3, 2, 2, 200, Expressive\Twig\TwigRenderer::class], 129 | 'zend-view-full' => [3, 2, 3, 200, Expressive\ZendView\ZendViewRenderer::class], 130 | ]; 131 | // @codingStandardsIgnoreEnd 132 | 133 | // Non-minimal installation types 134 | $types = [ 135 | OptionalPackages::INSTALL_FLAT, 136 | OptionalPackages::INSTALL_MODULAR, 137 | ]; 138 | 139 | // Execute a test case for each install type 140 | foreach ($types as $type) { 141 | foreach ($testCases as $testName => $arguments) { 142 | array_unshift($arguments, $type); 143 | $name = sprintf('%s-%s', $type, $testName); 144 | yield $name => $arguments; 145 | } 146 | } 147 | } 148 | 149 | public function injectRouterConfigProvider() 150 | { 151 | $configFile = $this->projectRoot . '/config/config.php'; 152 | $contents = file_get_contents($configFile); 153 | $contents = preg_replace( 154 | '/(new ConfigAggregator\(\[)/s', 155 | '$1' . "\n " . Expressive\Router\FastRouteRouter\ConfigProvider::class . "::class,\n", 156 | $contents 157 | ); 158 | file_put_contents($configFile, $contents); 159 | } 160 | 161 | public function injectConfigProvider(string $rendererClass) 162 | { 163 | $configFile = $this->projectRoot . '/config/config.php'; 164 | $contents = file_get_contents($configFile); 165 | $contents = preg_replace( 166 | '/(new ConfigAggregator\(\[)/s', 167 | '$1' . "\n " . $this->templateConfigProviders[$rendererClass] . "::class,\n", 168 | $contents 169 | ); 170 | file_put_contents($configFile, $contents); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /test/ExpressiveInstallerTest/UpdateRootPackageTest.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'foo/bar', 23 | 'baz/bat', 24 | ], 25 | 'composerDevRequires' => [ 26 | 'rab/oof', 27 | 'tab/zab', 28 | ], 29 | 'stabilityFlags' => [ 30 | 'foo/bar' => 'stable', 31 | 'baz/bat' => 'beta', 32 | ], 33 | 'composerDefinition' => [ 34 | 'autoload' => [ 35 | 'psr-4' => [ 36 | 'ExpressiveInstaller\\' => __DIR__, 37 | ], 38 | ], 39 | 'autoload-dev' => [ 40 | 'psr-4' => [ 41 | 'ExpressiveInstallerTest\\' => __DIR__, 42 | ], 43 | ], 44 | ], 45 | ]; 46 | 47 | public function testUpdateRootPackageWillUpdateComposedPackage() 48 | { 49 | $installer = $this->createOptionalPackages(); 50 | $this->setInstallerProperties($installer); 51 | 52 | $this->rootPackage->setRequires($this->changes['composerRequires'])->shouldBeCalled(); 53 | $this->rootPackage->setDevRequires($this->changes['composerDevRequires'])->shouldBeCalled(); 54 | $this->rootPackage->setStabilityFlags($this->changes['stabilityFlags'])->shouldBeCalled(); 55 | $this->rootPackage->setAutoload($this->changes['composerDefinition']['autoload'])->shouldBeCalled(); 56 | $this->rootPackage->setDevAutoload($this->changes['composerDefinition']['autoload-dev'])->shouldBeCalled(); 57 | $this->rootPackage->setExtra([])->shouldBeCalled(); 58 | 59 | $installer->updateRootPackage(); 60 | } 61 | 62 | protected function setInstallerProperties(OptionalPackages $installer) 63 | { 64 | foreach ($this->changes as $property => $value) { 65 | $this->setInstallerProperty($installer, $property, $value); 66 | } 67 | } 68 | 69 | protected function setInstallerProperty(OptionalPackages $installer, $property, $value) 70 | { 71 | $r = new ReflectionProperty($installer, $property); 72 | $r->setAccessible(true); 73 | $r->setValue($installer, $value); 74 | } 75 | } 76 | --------------------------------------------------------------------------------