* initial commit

This commit is contained in:
Danyi Dávid 2016-07-31 20:47:25 +02:00
commit f3939bbd13
62 changed files with 6827 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.idea
composer.phar
phpunit.xml
vendor/

28
.travis.yml Normal file
View File

@ -0,0 +1,28 @@
sudo: false
language: php
matrix:
fast_finish: true
include:
- php: 5.5
- php: 5.6
env:
- EXECUTE_CS_CHECK=true
- php: 7
- php: hhvm
allow_failures:
- php: hhvm
before_install:
- composer self-update
install:
- travis_retry composer install --no-interaction --ignore-platform-reqs --prefer-source --no-scripts
script:
- composer test
- if [[ $EXECUTE_CS_CHECK == 'true' ]]; then composer cs ; fi
notifications:
email: true

526
CHANGELOG.md Normal file
View File

@ -0,0 +1,526 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.0.2 - 2016-04-21
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#85](https://github.com/zendframework/zend-expressive-skeleton/pull/85)
updates the Aura.Di dependency to stable 3.X versions.
- [#88](https://github.com/zendframework/zend-expressive-skeleton/pull/88)
modifies the installer to remove `composer.lock` from the `.gitignore` file
during initial installation.
- [#89](https://github.com/zendframework/zend-expressive-skeleton/pull/89)
updates the zend-stdlib dependency to allow usage of its v3 series.
## 1.0.1 - 2016-03-17
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#53](https://github.com/zendframework/zend-expressive-skeleton/pull/53)
updates the default Pimple container script such that it now caches factory
instances for re-use.
- [#72](https://github.com/zendframework/zend-expressive-skeleton/pull/72)
updates the `composer.json` to remove the possibility of installing an
Expressive RC version, updates zend-servicemanager to allow using 3.0
versions, and updates whoops to allow either 1.1 or 2.0 versions.
- [#80](https://github.com/zendframework/zend-expressive-skeleton/pull/80)
updates the default ProxyManager constraints to also allow v2 versions.
- [#81](https://github.com/zendframework/zend-expressive-skeleton/pull/81)
fixes an issue in the installer whereby specified constraints were not being
passed to Composer prior to dependency resolution/installation, resulting in
stale dependencies.
- [#78](https://github.com/zendframework/zend-expressive-skeleton/pull/78)
updates the shipped default error templates to remove error/exception display.
Users who really need this functionality can write their own templates; the
project aims to deliver a "safe by default" setting.
## 1.0.0 - 2016-01-28
First stable release.
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#69](https://github.com/zendframework/zend-expressive-skeleton/pull/69)
updates the links in templates to point to the new documentation site on
https://zendframework.github.io/zend-expressive/ instead of rtfd.org.
## 1.0.0rc8 - 2016-01-21
Eighth release candidate.
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#66](https://github.com/zendframework/zend-expressive-skeleton/pull/66)
adds the `'error' => true,` declaration to the `'error'` pipeline middleware
specification.
- [#67](https://github.com/zendframework/zend-expressive-skeleton/pull/67)
updates the `filp/whoops` dependency for installer development to `^1.1 || ^2.0`;
the two are compatible for our use cases, but we should prefer the latest
that can be installed. As 2.0 requires PHP 5.5.9, but our minimum PHP version
is 5.5.0, we must specify both.
## 1.0.0rc7 - 2016-01-19
Seventh release candidate.
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#64](https://github.com/zendframework/zend-expressive-skeleton/pull/64)
fixes the installer script to correctly rewrite the `require-dev` section
and ensure only the development dependencies selected, as well as base
requirements such as PHPUnit and PHP_CodeSniffer, are installed. As such,
the `--no-dev` flag is no longer required, and development dependencies
such as whoops are properly installed.
## 1.0.0rc6 - 2016-01-19
Sixth release candidate.
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#56](https://github.com/zendframework/zend-expressive-skeleton/pull/56)
updates the `composer serve` command to include the `public/index.php` script
as an argument. This ensures that asset paths that the application could
intercept and serve will be passed to the application (previously, the
built-in server would treat these as 404s, and never pass them to the
application).
- [#57](https://github.com/zendframework/zend-expressive-skeleton/pull/57)
updates the Apache configuration rules defined in `public/.htaccess` to omit
several that could prevent the application from intercepting requests for
assets.
- [#52](https://github.com/zendframework/zend-expressive-skeleton/pull/52)
fixes the switch statement in the `HomePageAction` class to ensure the
template name and documentation link are accurately found.
- [#59](https://github.com/zendframework/zend-expressive-skeleton/pull/59)
updates the `config/container.php` implementation for zend-servicemanager such
that it can work with either v2 or v3 of that library.
- [#60](https://github.com/zendframework/zend-expressive-skeleton/pull/60)
updates the zend-expressive-helpers dependency to `^2.0`, and updates the
`config/autoload/middleware-pipeline.global.php` to follow the changes in
middleware configuration introduced in [zend-expressive #270](https://github.com/zendframework/zend-expressive/pull/270).
The change introduces convention-based keys for "always" (execute before
routing), "routing" (routing, listeners that act on the route result, and
dispatching), and "error", with reasonable priorities to ensure execution
order.
- [#60](https://github.com/zendframework/zend-expressive-skeleton/pull/60)
fixes the documentation for `composer create-project` to include the
`--no-dev` flag; this is done as composer currently installs the development
dependencies listed before the installer script rewrites the `composer.json`
file. Running `composer update` or `composer install` within the project
directory after the initial installation will install the development
dependencies.
## 1.0.0rc5 - 2015-12-22
Fifth release candidate.
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#42](https://github.com/zendframework/zend-expressive-skeleton/pull/42)
fixes some grammatical issues in the questions presented by the installer.
- [#45](https://github.com/zendframework/zend-expressive-skeleton/pull/45)
fixes how JS and CSS assets are added to zend-view templates.
- [#48](https://github.com/zendframework/zend-expressive-skeleton/pull/48)
adds unit tests for the `OptionalPackages` class (which provides the Composer
installer scripts).
- [#49](https://github.com/zendframework/zend-expressive-skeleton/pull/49)
updates the Pimple support to Pimple v3, ensuring Pimple users are using the
latest stable release.
## 1.0.0rc4 - 2015-12-09
Fourth release candidate.
### Added
- [#34](https://github.com/zendframework/zend-expressive-skeleton/pull/34)
updates the zend-view configuration to register a factory for
`Zend\View\HelperPluginManager`, as well as a `view_helpers` sub-key for
registering custom view helpers.
- [#37](https://github.com/zendframework/zend-expressive-skeleton/pull/37)
creates the subdirectories `src/App/` and `test/AppTest/`, moving the
subdirectories of each under those, and updating the `composer.json`
autoloading directives accordingly. This change will allow new projects to
implement a "modular" structure if desired, with a subdirectory per namespace.
- [#41](https://github.com/zendframework/zend-expressive-skeleton/pull/41) adds
the composer script "serve", which fires up the built-in PHP webserver on port
8080; invoke using `composer serve`.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#23](https://github.com/zendframework/zend-expressive-skeleton/pull/23)
updates the comment for the glob statements to ensure all 4 (not just 2!)
possible matches are detailed.
- [#24](https://github.com/zendframework/zend-expressive-skeleton/pull/24)
updates the `config/config.php` file to store cached configuration as a plain
PHP file, so that it can simply `include()`; this will be faster than using
JSON-serialized structures.
- [#30](https://github.com/zendframework/zend-expressive-skeleton/pull/30)
updates the Twig configuration to follow the changes made for
[zendframework/zend-expressive-twigrenderer 0.3.0](https://github.com/zendframework/zend-expressive-twigrenderer/releases/tag/0.3.0).
The old configuration format will still work, though users *should* update
their configuration to the new format. The change in this patch only affects
new installs.
- [#33](https://github.com/zendframework/zend-expressive-skeleton/pull/33)
updates to zendframework/zend-expressive-helpers `^1.2`.
- [#33](https://github.com/zendframework/zend-expressive-skeleton/pull/33) adds
configuration for auto-registering the new `Zend\Expressive\Helper\UrlHelperMiddleware`
as pipeline middleware; this fixes an issue when using the zend-view renderer
with the `url()` helper whereby the `UrlHelper` was being registered as a
route result observer too late to receive the `RouteResult`.
- [#40](https://github.com/zendframework/zend-expressive-skeleton/pull/40)
renames the namespace for the installer to `ExpressiveInstaller`.
## 1.0.0rc3 - 2015-12-07
Third release candidate.
### Added
- [#20](https://github.com/zendframework/zend-expressive-skeleton/pull/20) adds
the ability to specify a "minimal" install; when selected, the installer will
install modified configuration, omit some files, and remove the default
middleware and public assets.
- [#27](https://github.com/zendframework/zend-expressive-skeleton/pull/27) adds
[zendframework/zend-expressive-helpers](https://github.com/zendframework/zend-expressive-helpers)
as a dependency, and integrates the helpers into the configuration.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#13](https://github.com/zendframework/zend-expressive-skeleton/pull/13)
updates the installer to also remove the dependency on composer/composer
on completion.
- [#11](https://github.com/zendframework/zend-expressive-skeleton/pull/11)
moves the route middleware service definitions into the routes configuration
files.
- [#21](https://github.com/zendframework/zend-expressive-skeleton/pull/21)
updates `require` statements in generated configuration files to use the
`__DIR__` constant to ensure files are located relative to the origin file.
- [#25](https://github.com/zendframework/zend-expressive-skeleton/pull/25) and
[#29](https://github.com/zendframework/zend-expressive-skeleton/pull/29)
update minimum versions for each router and template implementation (final
versions for RC3 are all at `^1.0`).
- [#29](https://github.com/zendframework/zend-expressive-skeleton/pull/29) sets
the zend-expressive required version to `~1.0.0@rc || ^1.0`, to ensure a
stable version is always installed.
## 1.0.0rc2 - 2015-10-20
Second release candidate.
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Updated expressive to RC2.
- Updated subcomponent versions in installer to `^0.2`
## 1.0.0rc1 - 2015-10-19
First release candidate.
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 0.5.3 - 2015-10-16
### Added
- [#8](https://github.com/zendframework/zend-expressive-skeleton/pull/8) adds a
routine to the installer that recursively removes the `src/Composer/`
directory of the skeleton, ensuring you have a clean start when creating a
project.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 0.5.2 - 2015-10-13
### Added
- [#7](https://github.com/zendframework/zend-expressive-skeleton/pull/7) adds a
dependency on zend-stdlib for the purposes of globbing and merging
configuration.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 0.5.1 - 2015-10-11
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#6](https://github.com/zendframework/zend-expressive-skeleton/pull/6) updates
the zendframework/zend-view package configuration to remove the dependency on
zendframework/zend-i18n, as it is now handled in the standalone
zend-expressive-zendviewrenderer package.
## 0.5.0 - 2015-10-10
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#3](https://github.com/zendframework/zend-expressive-skeleton/pull/3) updates
the skeleton to use zendframework/zend-expressive 0.4.0.
## 0.4.0 - 2015-10-09
First release as zend-expressive-skeleton.
### Added
- Nothing.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 0.3.0 - 2015-09-12
### Added
- Use zend-expressive template factories.
- Use the zend view url helper in the layout template.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 0.2.0 - 2015-09-11
### Added
- [#bbb2e60](https://github.com/xtreamwayz/expressive-composer-installer/commit/bbb2e607af23e3ae23f6a9c71eb97c3c651c0ca1) adds PHPUnit tests.
- [#791c1c6](https://github.com/xtreamwayz/expressive-composer-installer/commit/791c1c63f324ca08d08e26375f3a356102bf2ad9) adds Whoops error handler.
- [e1d8d7bf](https://github.com/xtreamwayz/expressive-composer-installer/commit/e1d8d7bf5d5e2f51863fa59a37d1963405743201) adds config caching in production mode.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.
## 0.1.1 - 2015-09-08
### Added
- [#b4a0923](https://github.com/xtreamwayz/expressive-composer-installer/commit/b4a092386993227f8057d7ad4e0d9762659eefb0) adds support for Pimple 3.0.x. Still needs testing!
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- [#11](https://github.com/xtreamwayz/expressive-composer-installer/issues/11) fixes an issues where non stable packages are not being installed correctly.
## 0.1.0 - 2015-09-07
Initial tagged release.
### Added
- Everything.
### Deprecated
- Nothing.
### Removed
- Nothing.
### Fixed
- Nothing.

43
CONDUCT.md Normal file
View File

@ -0,0 +1,43 @@
# Contributor Code of Conduct
The Zend Framework project adheres to [The Code Manifesto](http://codemanifesto.com)
as its guidelines for contributor interactions.
## The Code Manifesto
We want to work in an ecosystem that empowers developers to reach their
potential — one that encourages growth and effective collaboration. A space that
is safe for all.
A space such as this benefits everyone that participates in it. It encourages
new developers to enter our field. It is through discussion and collaboration
that we grow, and through growth that we improve.
In the effort to create such a place, we hold to these values:
1. **Discrimination limits us.** This includes discrimination on the basis of
race, gender, sexual orientation, gender identity, age, nationality, technology
and any other arbitrary exclusion of a group of people.
2. **Boundaries honor us.** Your comfort levels are not everyones comfort
levels. Remember that, and if brought to your attention, heed it.
3. **We are our biggest assets.** None of us were born masters of our trade.
Each of us has been helped along the way. Return that favor, when and where
you can.
4. **We are resources for the future.** As an extension of #3, share what you
know. Make yourself a resource to help those that come after you.
5. **Respect defines us.** Treat others as you wish to be treated. Make your
discussions, criticisms and debates from a position of respectfulness. Ask
yourself, is it true? Is it necessary? Is it constructive? Anything less is
unacceptable.
6. **Reactions require grace.** Angry responses are valid, but abusive language
and vindictive actions are toxic. When something happens that offends you,
handle it assertively, but be respectful. Escalate reasonably, and try to
allow the offender an opportunity to explain themselves, and possibly correct
the issue.
7. **Opinions are just that: opinions.** Each and every one of us, due to our
background and upbringing, have varying opinions. The fact of the matter, is
that is perfectly acceptable. Remember this: if you respect your own
opinions, you should respect the opinions of others.
8. **To err is human.** You might not intend it, but mistakes do happen and
contribute to build experience. Tolerate honest mistakes, and don't hesitate
to apologize if you make one yourself.

233
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,233 @@
# CONTRIBUTING
## RESOURCES
If you wish to contribute to Zend Framework, please be sure to
read/subscribe to the following resources:
- [Coding Standards](https://github.com/zendframework/zf2/wiki/Coding-Standards)
- [Contributor's Guide](CONTRIBUTING.md)
- ZF Contributor's mailing list:
Archives: http://zend-framework-community.634137.n4.nabble.com/ZF-Contributor-f680267.html
Subscribe: zf-contributors-subscribe@lists.zend.com
- ZF Contributor's IRC channel:
#zftalk.dev on Freenode.net
If you are working on new features or refactoring [create a proposal](https://github.com/zendframework/zend-expressive-skeleton/issues/new).
## Reporting Potential Security Issues
If you have encountered a potential security vulnerability, please **DO NOT** report it on the public
issue tracker: send it to us at [zf-security@zend.com](mailto:zf-security@zend.com) instead.
We will work with you to verify the vulnerability and patch it as soon as possible.
When reporting issues, please provide the following information:
- Component(s) affected
- A description indicating how to reproduce the issue
- A summary of the security vulnerability and impact
We request that you contact us via the email address above and give the project
contributors a chance to resolve the vulnerability and issue a new release prior
to any public exposure; this helps protect users and provides them with a chance
to upgrade and/or update in order to protect their applications.
For sensitive email communications, please use [our PGP key](http://framework.zend.com/zf-security-pgp-key.asc).
## RUNNING TESTS
To run tests:
- Clone the repository:
```console
$ git clone git@github.com:zendframework/zend-expressive-skeleton.git
$ cd zend-expressive-skeleton
```
- Install dependencies via composer:
```console
$ composer install
```
**NOTE:** If you are wanting to test the installer itself, add the
`--no-scripts` flag to the `composer install` command.
If you don't have `curl` installed, you can also download `composer.phar` from
https://getcomposer.org/:
```console
$ curl -sS https://getcomposer.org/installer | php --
$ ln -s composer.phar composer
```
- Run the tests using the "test" command shipped in the `composer.json`:
```console
$ composer test
```
You can turn on conditional tests with the `phpunit.xml` file.
To do so:
- Copy `phpunit.xml.dist` file to `phpunit.xml`
- Edit `phpunit.xml` to enable any specific functionality you
want to test, as well as to provide test values to utilize.
## Running Coding Standards Checks
First, ensure you've installed dependencies via composer, per the previous
section on running tests.
To run CS checks only:
```console
$ composer cs
```
To attempt to automatically fix common CS issues:
```console
$ composer cs-fix
```
If the above fixes any CS issues, please re-run the tests to ensure
they pass, and make sure you add and commit the changes after verification.
## Recommended Workflow for Contributions
Your first step is to establish a public repository from which we can
pull your work into the master repository. We recommend using
[GitHub](https://github.com), as that is where the component is already hosted.
1. Setup a [GitHub account](http://github.com/), if you haven't yet
2. Fork the repository (http://github.com/zendframework/zend-expressive-skeleton)
3. Clone the canonical repository locally and enter it.
```console
$ git clone git://github.com:zendframework/zend-expressive-skeleton.git
$ cd zend-expressive-skeleton
```
4. Add a remote to your fork; substitute your GitHub username in the command
below.
```console
$ git remote add {username} git@github.com:{username}/zend-expressive-skeleton.git
$ git fetch {username}
```
### Keeping Up-to-Date
Periodically, you should update your fork or personal repository to
match the canonical ZF repository. Assuming you have setup your local repository
per the instructions above, you can do the following:
```console
$ git checkout master
$ git fetch origin
$ git rebase origin/master
# OPTIONALLY, to keep your remote up-to-date -
$ git push {username} master:master
```
If you're tracking other branches -- for example, the "develop" branch, where
new feature development occurs -- you'll want to do the same operations for that
branch; simply substitute "develop" for "master".
### Working on a patch
We recommend you do each new feature or bugfix in a new branch. This simplifies
the task of code review as well as the task of merging your changes into the
canonical repository.
A typical workflow will then consist of the following:
1. Create a new local branch based off either your master or develop branch.
2. Switch to your new local branch. (This step can be combined with the
previous step with the use of `git checkout -b`.)
3. Do some work, commit, repeat as necessary.
4. Push the local branch to your remote repository.
5. Send a pull request.
The mechanics of this process are actually quite trivial. Below, we will
create a branch for fixing an issue in the tracker.
```console
$ git checkout -b hotfix/9295
Switched to a new branch 'hotfix/9295'
```
... do some work ...
```console
$ git commit
```
... write your log message ...
```console
$ git push {username} hotfix/9295:hotfix/9295
Counting objects: 38, done.
Delta compression using up to 2 threads.
Compression objects: 100% (18/18), done.
Writing objects: 100% (20/20), 8.19KiB, done.
Total 20 (delta 12), reused 0 (delta 0)
To ssh://git@github.com/{username}/zend-expressive-skeleton.git
b5583aa..4f51698 HEAD -> master
```
To send a pull request, you have two options.
If using GitHub, you can do the pull request from there. Navigate to
your repository, select the branch you just created, and then select the
"Pull Request" button in the upper right. Select the user/organization
"zendframework" as the recipient.
If using your own repository - or even if using GitHub - you can use `git
format-patch` to create a patchset for us to apply; in fact, this is
**recommended** for security-related patches. If you use `format-patch`, please
send the patches as attachments to:
- zf-devteam@zend.com for patches without security implications
- zf-security@zend.com for security patches
#### What branch to issue the pull request against?
Which branch should you issue a pull request against?
- For fixes against the stable release, issue the pull request against the
"master" branch.
- For new features, or fixes that introduce new elements to the public API (such
as new public methods or properties), issue the pull request against the
"develop" branch.
### Branch Cleanup
As you might imagine, if you are a frequent contributor, you'll start to
get a ton of branches both locally and on your remote.
Once you know that your changes have been accepted to the master
repository, we suggest doing some cleanup of these branches.
- Local branch cleanup
```console
$ git branch -d <branchname>
```
- Remote branch removal
```console
$ git push {username} :<branchname>
```
## Conduct
Please see our [CONDUCT.md](CONDUCT.md) to understand expected behavior when interacting with others in the project.

12
LICENSE.md Normal file
View File

@ -0,0 +1,12 @@
Copyright (c) 2015, Zend Technologies USA, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
- Neither the name of Zend Technologies USA, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

88
README.md Normal file
View File

@ -0,0 +1,88 @@
# Expressive Skeleton and Installer
[![Build Status](https://secure.travis-ci.org/zendframework/zend-expressive-skeleton.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-expressive-skeleton)
*Begin developing PSR-7 middleware applications in seconds!*
[zend-expressive](https://github.com/zendframework/zend-expressive) builds on
[zend-stratigility](https://github.com/zendframework/zend-stratigility) to
provide a minimalist PSR-7 middleware framework for PHP with routing, DI
container, optional templating, and optional error handling capabilities.
This installer will setup a skeleton application based on zend-expressive by
choosing optional packages based on user input as demonstrated in the following
screenshot:
![screenshot-installer](https://cloud.githubusercontent.com/assets/459648/10410494/16bdc674-6f6d-11e5-8190-3c1466e93361.png)
The user selected packages are saved into `composer.json` so that everyone else
working on the project have the same packages installed. Configuration files and
templates are prepared for first use. The installer command is removed from
`composer.json` after setup succeeded, and all installer related files are
removed.
## Getting Started
Start your new Expressive project with composer:
```bash
$ composer create-project zendframework/zend-expressive-skeleton <project-path>
```
After choosing and installing the packages you want, go to the
`<project-path>` and start PHP's built-in web server to verify installation:
```bash
$ composer serve
```
You can then browse to http://localhost:8080.
> ### Setting a timeout
>
> Composer commands time out after 300 seconds (5 minutes). On Linux-based
> systems, the `php -S` command that `composer server` spawns continues running
> as a background process, but on other systems halts when the timeout occurs.
>
> If you want the server to live longer, you can use the
> `COMPOSER_PROCESS_TIMEOUT` environment variable when executing `composer
> serve` to extend the timeout. As an example, the following will extend it
> to a full day:
>
> ```bash
> $ COMPOSER_PROCESS_TIMEOUT=86400 composer serve
> ```
## Troubleshooting
If the installer fails during the ``composer create-project`` phase, please go
through the following list before opening a new issue. Most issues we have seen
so far can be solved by `self-update` and `clear-cache`.
1. Be sure to work with the latest version of composer by running `composer self-update`.
2. Try clearing Composer's cache by running `composer clear-cache`.
If neither of the above help, you might face more serious issues:
- Info about the [zlib_decode error](https://github.com/composer/composer/issues/4121).
- Info and solutions for [composer degraded mode](https://getcomposer.org/doc/articles/troubleshooting.md#degraded-mode).
## Skeleton Development
This section applies only if you cloned this repo with `git clone`, not when you
installed expressive with `composer create-project ...`.
If you want to run tests against the installer, you need to clone this repo and
setup all dependencies with composer. Make sure you **prevent composer running
scripts** with `--no-scripts`, otherwise it will remove the installer and all
tests.
```bash
$ composer install --no-scripts
$ composer test
```
Please note that the installer tests remove installed config files and templates
before and after running the tests.
Before contributing read [the contributing guide](CONTRIBUTING.md).

9
cli-config.php Normal file
View File

@ -0,0 +1,9 @@
<?php
$container = require 'config/container.php';
return new \Symfony\Component\Console\Helper\HelperSet([
'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper(
$container->get('doctrine.entity_manager.orm_default')
),
]);

58
composer.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "zendframework/zend-expressive-skeleton",
"description": "Zend expressive skelton. Begin developing PSR-7 middleware applications in seconds",
"type": "project",
"homepage": "https://github.com/zendframework/zend-expressive-skeleton",
"license": "BSD-3-Clause",
"authors": [
{
"name": "Geert Eltink",
"homepage": "https://xtreamwayz.com/"
}
],
"extra": {
"branch-alias": {
"dev-master": "1.0-dev",
"dev-develop": "1.1-dev"
}
},
"require": {
"php": "^5.5 || ^7.0",
"roave/security-advisories": "dev-master",
"zendframework/zend-expressive": "^1.0",
"zendframework/zend-expressive-helpers": "^2.0",
"zendframework/zend-stdlib": "^2.7 || ^3.0",
"zendframework/zend-expressive-fastroute": "^1.0",
"zendframework/zend-servicemanager": "^2.7.3 || ^3.0",
"ocramius/proxy-manager": "^1.0 || ^2.0",
"dasprid/container-interop-doctrine": "^0.2.2",
"zendframework/zend-json": "^3.0",
"zendframework/zend-hydrator": "^2.2",
"zendframework/zend-filter": "^2.7"
},
"require-dev": {
"phpunit/phpunit": "^4.8",
"squizlabs/php_codesniffer": "^2.3",
"filp/whoops": "^1.1 || ^2.0"
},
"autoload": {
"psr-4": {
"App\\": "src/App/"
}
},
"autoload-dev": {
"psr-4": {
"AppTest\\": "test/AppTest/"
}
},
"scripts": {
"check": [
"@cs",
"@test"
],
"cs": "phpcs",
"cs-fix": "phpcbf",
"serve": "php -S 0.0.0.0:8080 -t public/ public/index.php",
"test": "phpunit"
}
}

3128
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

2
config/autoload/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
local.php
*.local.php

View File

@ -0,0 +1,26 @@
<?php
use Zend\Expressive\Application;
use Zend\Expressive\Container\ApplicationFactory;
use Zend\Expressive\Helper;
return [
// Provides application-wide services.
// We recommend using fully-qualified class names whenever possible as
// service names.
'dependencies' => [
// Use 'invokables' for constructor-less services, or services that do
// not require arguments to the constructor. Map a service name to the
// class name.
'invokables' => [
// Fully\Qualified\InterfaceName::class => Fully\Qualified\ClassName::class,
Helper\ServerUrlHelper::class => Helper\ServerUrlHelper::class,
],
// Use 'factories' for services provided by callbacks/factory classes.
'factories' => [
Application::class => ApplicationFactory::class,
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
'doctrine.entity_manager.orm_default' => \ContainerInteropDoctrine\EntityManagerFactory::class,
'doctrine.hydrator' => \App\Hydrator\DoctrineObjectFactory::class,
],
],
];

View File

@ -0,0 +1,19 @@
<?php
return [
'doctrine' => [
'driver' => [
'orm_default' => [
'class' => \Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain::class,
'drivers' => [
'App\Entity' => 'my_entity',
],
],
'my_entity' => [
'class' => \Doctrine\ORM\Mapping\Driver\AnnotationDriver::class,
'cache' => 'array',
'paths' => __DIR__ . '/../../src/App/Entity',
],
],
],
];

View File

@ -0,0 +1,14 @@
<?php
return [
'doctrine' => [
'connection' => [
'orm_default' => [
'params' => [
'url' => 'mysqli://user:passwd@host/database',
'charset' => 'UTF8',
],
],
],
],
];

View File

@ -0,0 +1,21 @@
<?php
return [
'dependencies' => [
'invokables' => [
'Zend\Expressive\Whoops' => Whoops\Run::class,
'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class,
],
'factories' => [
'Zend\Expressive\FinalHandler' => Zend\Expressive\Container\WhoopsErrorHandlerFactory::class,
],
],
'whoops' => [
'json_exceptions' => [
'display' => true,
'show_trace' => true,
'ajax_only' => true,
],
],
];

View File

@ -0,0 +1,7 @@
<?php
return [
'debug' => true,
'config_cache_enabled' => false,
];

View File

@ -0,0 +1,69 @@
<?php
use Zend\Expressive\Container\ApplicationFactory;
use Zend\Expressive\Helper;
return [
'dependencies' => [
'factories' => [
Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class,
Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
],
],
// This can be used to seed pre- and/or post-routing middleware
'middleware_pipeline' => [
// An array of middleware to register. Each item is of the following
// specification:
//
// [
// Required:
// 'middleware' => 'Name or array of names of middleware services and/or callables',
// Optional:
// 'path' => '/path/to/match', // string; literal path prefix to match
// // middleware will not execute
// // if path does not match!
// 'error' => true, // boolean; true for error middleware
// 'priority' => 1, // int; higher values == register early;
// // lower/negative == register last;
// // default is 1, if none is provided.
// ],
//
// While the ApplicationFactory ignores the keys associated with
// specifications, they can be used to allow merging related values
// defined in multiple configuration files/locations. This file defines
// some conventional keys for middleware to execute early, routing
// middleware, and error middleware.
'always' => [
'middleware' => [
// Add more middleware here that you want to execute on
// every request:
// - bootstrapping
// - pre-conditions
// - modifications to outgoing responses
Helper\ServerUrlMiddleware::class,
],
'priority' => 10000,
],
'routing' => [
'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE,
Helper\UrlHelperMiddleware::class,
// Add more middleware here that needs to introspect the routing
// results; this might include:
// - route-based authentication
// - route-based validation
// - etc.
ApplicationFactory::DISPATCH_MIDDLEWARE,
],
'priority' => 1,
],
'error' => [
'middleware' => [
// Add error middleware here.
],
'error' => true,
'priority' => -10000,
],
],
];

View File

@ -0,0 +1,62 @@
<?php
return [
'dependencies' => [
'invokables' => [
Zend\Expressive\Router\RouterInterface::class => Zend\Expressive\Router\FastRouteRouter::class,
App\Action\PingAction::class => App\Action\PingAction::class,
],
'factories' => [
App\Action\HomePageAction::class => App\Action\HomePageFactory::class,
App\Action\Article\ListAction::class => App\Action\Article\ListFactory::class,
App\Action\Article\GetAction::class => App\Action\Article\GetFactory::class,
App\Action\Article\PostAction::class => App\Action\Article\PostFactory::class,
App\Action\Article\PutAction::class => App\Action\Article\PutFactory::class,
App\Action\Article\DeleteAction::class => App\Action\Article\DeleteFactory::class,
],
],
'routes' => [
[
'name' => 'home',
'path' => '/',
'middleware' => App\Action\HomePageAction::class,
'allowed_methods' => ['GET'],
],
[
'name' => 'api.article.list',
'path' => '/api/article',
'middleware' => App\Action\Article\ListAction::class,
'allowed_methods' => ['GET'],
],
[
'name' => 'api.article.get',
'path' => '/api/article/{id:\d+}',
'middleware' => App\Action\Article\GetAction::class,
'allowed_methods' => ['GET'],
],
[
'name' => 'api.article.add',
'path' => '/api/article',
'middleware' => App\Action\Article\PostAction::class,
'allowed_methods' => ['POST'],
],
[
'name' => 'api.article.update',
'path' => '/api/article/{id:\d+}',
'middleware' => App\Action\Article\PutAction::class,
'allowed_methods' => ['PUT'],
],
[
'name' => 'api.article.delete',
'path' => '/api/article/{id:\d+}',
'middleware' => App\Action\Article\DeleteAction::class,
'allowed_methods' => ['DELETE'],
],
[
'name' => 'api.ping',
'path' => '/api/ping',
'middleware' => App\Action\PingAction::class,
'allowed_methods' => ['GET'],
],
],
];

View File

@ -0,0 +1,14 @@
<?php
return [
'debug' => false,
'config_cache_enabled' => false,
'zend-expressive' => [
'error_handler' => [
'template_404' => 'error::404',
'template_error' => 'error::error',
],
],
];

35
config/config.php Normal file
View File

@ -0,0 +1,35 @@
<?php
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\Glob;
/**
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
* then ``local.php`` and finally ``*.local.php``. This way local settings overwrite global settings.
*
* The configuration can be cached. This can be done by setting ``config_cache_enabled`` to ``true``.
*
* Obviously, if you use closures in your config you can't cache it.
*/
$cachedConfigFile = 'data/cache/app_config.php';
$config = [];
if (is_file($cachedConfigFile)) {
// Try to load the cached config
$config = include $cachedConfigFile;
} else {
// Load configuration from autoload path
foreach (Glob::glob('config/autoload/{{,*.}global,{,*.}local}.php', Glob::GLOB_BRACE) as $file) {
$config = ArrayUtils::merge($config, include $file);
}
// Cache config if enabled
if (isset($config['config_cache_enabled']) && $config['config_cache_enabled'] === true) {
file_put_contents($cachedConfigFile, '<?php return ' . var_export($config, true) . ';');
}
}
// Return an ArrayObject so we can inject the config as a service in Aura.Di
// and still use array checks like ``is_array``.
return new ArrayObject($config, ArrayObject::ARRAY_AS_PROPS);

16
config/container.php Normal file
View File

@ -0,0 +1,16 @@
<?php
use Zend\ServiceManager\Config;
use Zend\ServiceManager\ServiceManager;
// Load configuration
$config = require __DIR__ . '/config.php';
// Build container
$container = new ServiceManager();
(new Config($config['dependencies']))->configureServiceManager($container);
// Inject config
$container->setService('config', $config);
return $container;

2
data/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

View File

@ -0,0 +1,9 @@
auxiliary.org-netbeans-modules-php-editor.fluent_2e_setter_2e_project_2e_property=true
auxiliary.org-netbeans-modules-php-editor.getter_2e_setter_2e_method_2e_name_2e_generation=AS_JAVA
auxiliary.org-netbeans-modules-php-editor.public_2e_modifier_2e_project_2e_property=true
copy.src.files=false
copy.src.on.open=false
copy.src.target=/var/www/simen-backend-middleware
index.file=
run.as=LOCAL
url=http://localhost/simen-backend-middleware/

View File

@ -0,0 +1,7 @@
include.path=${php.global.include.path}
php.version=PHP_56
source.encoding=UTF-8
src.dir=.
tags.asp=false
tags.short=false
web.root=.

9
nbproject/project.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://www.netbeans.org/ns/project/1">
<type>org.netbeans.modules.php.project</type>
<configuration>
<data xmlns="http://www.netbeans.org/ns/php-project/1">
<name>simen-backend-middleware</name>
</data>
</configuration>
</project>

20
phpcs.xml Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0"?>
<ruleset name="Zend Framework coding standard">
<description>Zend Framework coding standard</description>
<!-- display progress -->
<arg value="p"/>
<arg name="colors"/>
<!-- inherit rules from: -->
<rule ref="PSR2"/>
<rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
<rule ref="Squiz.WhiteSpace.SuperfluousWhitespace">
<properties>
<property name="ignoreBlankLines" value="false"/>
</properties>
</rule>
<!-- Paths to check -->
<file>src</file>
</ruleset>

13
phpunit.xml.dist Normal file
View File

@ -0,0 +1,13 @@
<phpunit bootstrap="./vendor/autoload.php" colors="true">
<testsuites>
<testsuite name="App\\Tests">
<directory>./test</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>

17
public/.htaccess Normal file
View File

@ -0,0 +1,17 @@
RewriteEngine On
# The following rule tells Apache that if the requested filename
# exists, simply serve it.
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
# The following rewrites all other queries to index.php. The
# condition ensures that if you are using Apache aliases to do
# mass virtual hosting, the base path will be prepended to
# allow proper resolution of the index.php file; it will work
# in non-aliased environments as well, providing a safe, one-size
# fits all solution.
RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$
RewriteRule ^(.*) - [E=BASE:%1]
RewriteRule ^(.*)$ %{ENV:BASE}index.php [NC,L]

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

18
public/index.php Normal file
View File

@ -0,0 +1,18 @@
<?php
// Delegate static file requests back to the PHP built-in webserver
if (php_sapi_name() === 'cli-server'
&& is_file(__DIR__ . parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH))
) {
return false;
}
chdir(dirname(__DIR__));
require 'vendor/autoload.php';
/** @var \Interop\Container\ContainerInterface $container */
$container = require 'config/container.php';
/** @var \Zend\Expressive\Application $app */
$app = $container->get(\Zend\Expressive\Application::class);
$app->run();

BIN
public/zf-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

View File

@ -0,0 +1,27 @@
<?php
namespace App\Action;
use Interop\Container\ContainerInterface;
abstract class AbstractFactory
{
/**
* @param ContainerInterface $container
* @return \Doctrine\ORM\EntityManager
*/
protected function getEntityManager(ContainerInterface $container)
{
return $container->get('doctrine.entity_manager.orm_default');
}
/**
* @param ContainerInterface $container
* @return \App\Hydrator\DoctrineObject
*/
protected function getDoctrineHydrator(ContainerInterface $container)
{
return $container->get('doctrine.hydrator');
}
}

View File

@ -0,0 +1,74 @@
<?php
namespace App\Action;
use Exception;
abstract class AbstractFormAction
{
/**
*
* @param type $request
* @return type
*/
public function getRequestData($request)
{
$body = $request->getParsedBody();
if (!empty($body)) {
return $body;
}
return $this->parseRequestData(
$request->getBody()->getContents(),
$request->getHeaderLine('content-type')
);
}
/**
*
* @param type $input
* @param type $contentType
* @return type
*/
public function parseRequestData($input, $contentType)
{
$contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType);
$parser = $this->returnParserContentType($contentTypeParts[0]);
return $parser($input);
}
/**
*
* @param type $contentType
* @return type
*/
public function returnParserContentType($contentType)
{
if ($contentType === 'application/x-www-form-urlencoded') {
return function ($input) {
parse_str($input, $data);
return $data;
};
} elseif ($contentType === 'application/json') {
return function ($input) {
$jsonDecoder = new \Zend\Json\Json();
try {
return $jsonDecoder->decode($input, \Zend\Json\Json::TYPE_ARRAY);
} catch (Exception $e) {
return new ApiProblem(400, 'Data Parsing Error.');
}
};
} elseif ($contentType === 'multipart/form-data') {
return function ($input) {
return $input;
};
}
return function ($input) {
return $input;
};
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace App\Action\Article;
use App\Entity\Article;
use Doctrine\ORM\EntityManager;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
class DeleteAction
{
/**
* @var EntityManager
*/
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
{
$id = $request->getAttribute('id');
if (null === ($entity = $this->em->find(Article::class, $id))) {
$ret = new JsonResponse([
'success' => false
]);
return $ret->withStatus(404);
}
try {
$this->em->remove($entity);
$this->em->flush();
} catch (\Exception $ex) {
$ret = new JsonResponse([
'success' => false
]);
return $ret->withStatus(500);
}
return new JsonResponse([
'success' => true,
]);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Action\Article;
use App\Action\AbstractFactory;
use App\Action\Article\DeleteAction;
use Interop\Container\ContainerInterface;
class DeleteFactory extends AbstractFactory
{
public function __invoke(ContainerInterface $container)
{
$em = $this->getEntityManager($container);
return new DeleteAction($em);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace App\Action\Article;
use App\Entity\Article;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
class GetAction
{
/**
* @var EntityManager
*/
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
{
$id = $request->getAttribute('id');
$qb = $this->em->createQueryBuilder();
$entity = $qb->select('a,u,c')
->from(Article::class, 'a')
->leftJoin('a.author', 'u')
->leftJoin('a.comments', 'c')
->where('a.id = :aid')
->setParameter('aid', $id)
->getQuery()
->getOneOrNullResult(Query::HYDRATE_ARRAY);
if (null === $entity) {
$ret = new JsonResponse([
'success' => false
]);
return $ret->withStatus(404);
}
return new JsonResponse([
'success' => true,
'result' => $entity,
]);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Action\Article;
use App\Action\AbstractFactory;
use App\Action\Article\GetAction;
use Interop\Container\ContainerInterface;
class GetFactory extends AbstractFactory
{
public function __invoke(ContainerInterface $container)
{
$em = $this->getEntityManager($container);
return new GetAction($em);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Action\Article;
use App\Entity\Article;
use Doctrine\ORM\EntityManager;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
class ListAction
{
/**
* @var EntityManager
*/
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
{
$qb = $this->em->createQueryBuilder();
$entities = $qb->select('a,u,c')
->from(Article::class, 'a')
->leftJoin('a.author', 'u')
->leftJoin('a.comments', 'c')
->getQuery()
->getArrayResult();
return new JsonResponse([
'success' => true,
'result' => $entities,
]);
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Action\Article;
use App\Action\AbstractFactory;
use App\Action\Article\ListAction;
use Interop\Container\ContainerInterface;
class ListFactory extends AbstractFactory
{
public function __invoke(ContainerInterface $container)
{
$em = $this->getEntityManager($container);
return new ListAction($em);
}
}

View File

@ -0,0 +1,45 @@
<?php
namespace App\Action\Article;
use App\Action\AbstractFormAction;
use App\Entity\Article;
use App\Hydrator\DoctrineObject;
use Doctrine\ORM\EntityManager;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
class PostAction extends AbstractFormAction
{
/**
* @var EntityManager
*/
private $em;
/**
* @var DoctrineObject
*/
private $hydrator;
public function __construct(EntityManager $em, DoctrineObject $hydrator)
{
$this->em = $em;
$this->hydrator = $hydrator;
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
{
$data = $this->getRequestData($request);
$entity = $this->hydrator->hydrate($data, new Article());
$this->em->persist($entity);
$this->em->flush();
return new JsonResponse([
'success' => true,
'result' => $entity,
]);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Action\Article;
use App\Action\AbstractFactory;
use App\Action\Article\PostAction;
use Interop\Container\ContainerInterface;
class PostFactory extends AbstractFactory
{
public function __invoke(ContainerInterface $container)
{
$em = $this->getEntityManager($container);
$hydrator = $this->getDoctrineHydrator($container);
return new PostAction($em, $hydrator);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace App\Action\Article;
use App\Action\AbstractFormAction;
use App\Entity\Article;
use App\Hydrator\DoctrineObject;
use Doctrine\ORM\EntityManager;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
class PutAction extends AbstractFormAction
{
/**
* @var EntityManager
*/
private $em;
/**
* @var DoctrineObject
*/
private $hydrator;
public function __construct(EntityManager $em, DoctrineObject $hydrator)
{
$this->em = $em;
$this->hydrator = $hydrator;
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
{
$id = $request->getAttribute('id', false);
if (null === ($entity = $this->em->find(Article::class, $id))) {
$ret = new JsonResponse([
'success' => false
]);
return $ret->withStatus(404);
}
$data = $this->getRequestData($request);
$entity = $this->hydrator->hydrate($data, $entity);
$this->em->persist($entity);
$this->em->flush();
return new JsonResponse([
'success' => true,
'result' => $entity,
]);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Action\Article;
use App\Action\AbstractFactory;
use App\Action\Article\PutAction;
use Interop\Container\ContainerInterface;
class PutFactory extends AbstractFactory
{
public function __invoke(ContainerInterface $container)
{
$em = $this->getEntityManager($container);
$hydrator = $this->getDoctrineHydrator($container);
return new PutAction($em, $hydrator);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Action;
use App\Entity\User;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
class HomePageAction
{
/**
* @var EntityManager
*/
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
{
$qb = $this->em->createQueryBuilder();
$user = $qb->select('u, a, ac, uc')
->from(User::class, 'u')
->leftJoin('u.comments', 'uc')
->leftJoin('u.articles', 'a')
->leftJoin('a.comments', 'ac')
->where('u.id = :uid')
->setParameter('uid', 1)
->getQuery()
->getSingleResult(Query::HYDRATE_ARRAY);
return new JsonResponse([
'welcome' => 'Congratulations! You have reached our API endpoint.',
'user' => $user,
]);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Action;
use App\Action\HomePageAction;
use Interop\Container\ContainerInterface;
class HomePageFactory
{
public function __invoke(ContainerInterface $container)
{
$em = $container->get('doctrine.entity_manager.orm_default');
return new HomePageAction($em);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Action;
use Zend\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class PingAction
{
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next = null)
{
return new JsonResponse(['ack' => time()]);
}
}

180
src/App/Entity/Article.php Normal file
View File

@ -0,0 +1,180 @@
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use JsonSerializable;
/**
* @ORM\Entity
* @ORM\Table(name="article")
*/
class Article implements JsonSerializable
{
use Traits\GetterSetter;
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="IDENTITY")
* @var int
*/
private $id;
/**
* @ORM\Column(name="title", type="string", length=255)
* @var string
*/
private $title = null;
/**
* @ORM\Column(name="content", type="string", length=65535)
* @var string
*/
private $content = null;
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="articles")
* @ORM\JoinColumn(name="author_id", referencedColumnName="id")
* @var User
*/
private $author = null;
/**
* @ORM\OneToMany(targetEntity="Comment", mappedBy="author")
* @ORM\JoinColumn(name="id", referencedColumnName="author_id", nullable=false)
* @var Comment[]
*/
private $comments;
/**
* @ORM\Column(name="visible", type="boolean")
* @var bool
*/
private $visible = true;
public function __construct()
{
$this->comments = new ArrayCollection();
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* @return User
*/
public function getAuthor()
{
return $this->author;
}
/**
* @return Comment[]
*/
public function getComments()
{
return $this->comments;
}
/**
* @return bool
*/
public function getVisible()
{
return $this->visible;
}
/**
* @param int $id
* @return Article
*/
public function setId(int $id)
{
$this->id = $id;
return $this;
}
/**
* @param string $title
* @return Article
*/
public function setTitle(string $title)
{
$this->title = $title;
return $this;
}
/**
* @param string $content
* @return Article
*/
public function setContent(string $content)
{
$this->content = $content;
return $this;
}
/**
* @param User $author
* @return Article
*/
public function setAuthor(User $author)
{
$this->author = $author;
return $this;
}
/**
* @param Comment[] $comments
* @return Article
*/
public function setComments(array $comments)
{
$this->comments = $comments;
return $this;
}
/**
* @param bool $visible
* @return Article
*/
public function setVisible(bool $visible)
{
$this->visible = $visible;
return $this;
}
public function jsonSerialize()
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
'author' => $this->author,
'comments' => $this->comments,
'visible' => $this->visible,
];
}
}

178
src/App/Entity/Comment.php Normal file
View File

@ -0,0 +1,178 @@
<?php
namespace App\Entity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use JsonSerializable;
/**
* @ORM\Entity
* @ORM\Table(name="comment")
*/
class Comment implements JsonSerializable
{
use Traits\GetterSetter;
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="IDENTITY")
* @var int
*/
private $id;
/**
* @ORM\Column(name="content", type="string", length=65535)
* @var string
*/
private $content = null;
/**
* @ORM\Column(name="created_at", type="datetime")
* @var DateTime
*/
private $createdAt = null;
/**
* @ORM\ManyToOne(targetEntity="Article", inversedBy="comments")
* @ORM\JoinColumn(name="article_id", referencedColumnName="id")
* @var Article
*/
private $article = null;
/**
* @ORM\ManyToOne(targetEntity="User", inversedBy="comments")
* @ORM\JoinColumn(name="author_id", referencedColumnName="id")
* @var User
*/
private $author = null;
/**
* @ORM\Column(name="visible", type="boolean")
* @var bool
*/
private $visible = true;
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getContent()
{
return $this->content;
}
/**
* @return DateTime
*/
public function getCreatedAt()
{
return $this->createdAt;
}
/**
* @return Article
*/
public function getArticle()
{
return $this->article;
}
/**
* @return User
*/
public function getAuthor()
{
return $this->author;
}
/**
* @return bool
*/
public function getVisible()
{
return $this->visible;
}
/**
* @param int $id
* @return Comment
*/
public function setId(int $id)
{
$this->id = $id;
return $this;
}
/**
* @param string $content
* @return Comment
*/
public function setContent(string $content)
{
$this->content = $content;
return $this;
}
/**
* @param DateTime $createdAt
* @return Comment
*/
public function setCreatedAt(DateTime $createdAt)
{
$this->createdAt = clone $createdAt;
return $this;
}
/**
* @param Article $article
* @return Comment
*/
public function setArticle(Article $article)
{
$this->article = $article;
return $this;
}
/**
* @param User $author
* @return Comment
*/
public function setAuthor(User $author)
{
$this->author = $author;
return $this;
}
/**
* @param bool $visible
* @return Comment
*/
public function setVisible(bool $visible)
{
$this->visible = $visible;
return $this;
}
/**
* @return array
*/
public function jsonSerialize()
{
return [
'id' => $this->id,
'content' => $this->content,
'createdAt' => $this->createdAt,
'author' => $this->author,
'article' => $this->article,
'visible' => $this->visible,
];
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Entity\Traits;
Trait GetterSetter
{
/**
* Returns the getter name for a field
*
* @param string $field
* @return string
*/
protected function getterName($field)
{
return sprintf('get%s', ucfirst(
str_replace(' ', '', ucwords(str_replace('_', ' ', $field)))
));
}
/**
* Returns the setter name for a field
*
* @param string $field
* @return string
*/
protected function setterName($field)
{
return sprintf('set%s', ucfirst(
str_replace(' ', '', ucwords(str_replace('_', ' ', $field)))
));
}
/**
* Populate entity with the given data.
* The set* method will be used to set the data.
*
* @param array $data
* @return boolean
*/
public function populate(array $data = [])
{
foreach ($data as $field => $value) {
$setter = $this->setterName($field);
if (method_exists($this, $setter)) {
$this->{$setter}($value);
}
}
return true;
}
}

184
src/App/Entity/User.php Normal file
View File

@ -0,0 +1,184 @@
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use JsonSerializable;
/**
* @ORM\Entity
* @ORM\Table(name="user")
*/
class User implements JsonSerializable
{
use Traits\GetterSetter;
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="IDENTITY")
* @var int
*/
private $id;
/**
* @ORM\Column(name="name", type="string", length=150)
* @var string
*/
private $name;
/**
* @ORM\Column(name="email", type="string", length=255)
* @var string
*/
private $email;
/**
* @ORM\OneToMany(targetEntity="Article", mappedBy="author")
* @ORM\JoinColumn(name="id", referencedColumnName="author_id", nullable=false)
* @var Article[]
*/
private $articles;
/**
* @ORM\OneToMany(targetEntity="Comment", mappedBy="author")
* @ORM\JoinColumn(name="id", referencedColumnName="author_id", nullable=false)
* @var Comment[]
*/
private $comments;
/**
* @ORM\Column(name="active", type="boolean")
* @var bool
*/
private $active = true;
public function __construct()
{
$this->articles = new ArrayCollection();
$this->comments = new ArrayCollection();
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @return string
*/
public function getEmail()
{
return $this->email;
}
/**
* @return Article[]
*/
public function getArticles()
{
return $this->articles;
}
/**
* @return Comment[]
*/
public function getComments()
{
return $this->comments;
}
/**
* @return bool
*/
public function getActive()
{
return $this->active;
}
/**
* @param int $id
* @return User
*/
public function setId(int $id)
{
$this->id = $id;
return $this;
}
/**
* @param string $name
* @return User
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* @param string $email
* @return User
*/
public function setEmail(string $email)
{
$this->email = $email;
return $this;
}
/**
* @param Article[] $articles
* @return User
*/
public function setArticles(array $articles)
{
$this->articles = $articles;
return $this;
}
/**
* @param Comment[] $comments
* @return User
*/
public function setComments(array $comments)
{
$this->comments = $comments;
return $this;
}
/**
* @param bool $active
* @return User
*/
public function setActive(bool $active)
{
$this->active = $active;
return $this;
}
/**
* @return array
*/
public function jsonSerialize()
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'articles' => $this->articles,
'comments' => $this->comments,
'active' => $this->active,
];
}
}

View File

@ -0,0 +1,595 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace App\Hydrator;
use DateTime;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Persistence\ObjectManager;
use Doctrine\Common\Util\Inflector;
use InvalidArgumentException;
use RuntimeException;
use Traversable;
use Zend\Stdlib\ArrayUtils;
use Zend\Hydrator\AbstractHydrator;
use Zend\Hydrator\Filter\FilterProviderInterface;
/**
* This hydrator has been completely refactored for DoctrineModule 0.7.0. It provides an easy and powerful way
* of extracting/hydrator objects in Doctrine, by handling most associations types.
*
* Starting from DoctrineModule 0.8.0, the hydrator can be used multiple times with different objects
*
* @license MIT
* @link http://www.doctrine-project.org/
* @since 0.7.0
* @author Michael Gallego <mic.gallego@gmail.com>
*/
class DoctrineObject extends AbstractHydrator
{
/**
* @var ObjectManager
*/
protected $objectManager;
/**
* @var ClassMetadata
*/
protected $metadata;
/**
* @var bool
*/
protected $byValue = true;
/**
* Constructor
*
* @param ObjectManager $objectManager The ObjectManager to use
* @param bool $byValue If set to true, hydrator will always use entity's public API
*/
public function __construct(ObjectManager $objectManager, $byValue = true)
{
parent::__construct();
$this->objectManager = $objectManager;
$this->byValue = (bool) $byValue;
}
/**
* Extract values from an object
*
* @param object $object
* @return array
*/
public function extract($object)
{
$this->prepare($object);
if ($this->byValue) {
return $this->extractByValue($object);
}
return $this->extractByReference($object);
}
/**
* Hydrate $object with the provided $data.
*
* @param array $data
* @param object $object
* @return object
*/
public function hydrate(array $data, $object)
{
$this->prepare($object);
if ($this->byValue) {
return $this->hydrateByValue($data, $object);
}
return $this->hydrateByReference($data, $object);
}
/**
* Prepare the hydrator by adding strategies to every collection valued associations
*
* @param object $object
* @return void
*/
protected function prepare($object)
{
$this->metadata = $this->objectManager->getClassMetadata(get_class($object));
$this->prepareStrategies();
}
/**
* Prepare strategies before the hydrator is used
*
* @throws \InvalidArgumentException
* @return void
*/
protected function prepareStrategies()
{
$associations = $this->metadata->getAssociationNames();
foreach ($associations as $association) {
if ($this->metadata->isCollectionValuedAssociation($association)) {
// Add a strategy if the association has none set by user
if (!$this->hasStrategy($association)) {
if ($this->byValue) {
$this->addStrategy($association, new Strategy\AllowRemoveByValue());
} else {
$this->addStrategy($association, new Strategy\AllowRemoveByReference());
}
}
$strategy = $this->getStrategy($association);
if (!$strategy instanceof Strategy\AbstractCollectionStrategy) {
throw new InvalidArgumentException(
sprintf(
'Strategies used for collections valued associations must inherit from '
. 'Strategy\AbstractCollectionStrategy, %s given',
get_class($strategy)
)
);
}
$strategy->setCollectionName($association)
->setClassMetadata($this->metadata);
}
}
}
/**
* Extract values from an object using a by-value logic (this means that it uses the entity
* API, in this case, getters)
*
* @param object $object
* @throws RuntimeException
* @return array
*/
protected function extractByValue($object)
{
$fieldNames = array_merge($this->metadata->getFieldNames(), $this->metadata->getAssociationNames());
$methods = get_class_methods($object);
$filter = $object instanceof FilterProviderInterface
? $object->getFilter()
: $this->filterComposite;
$data = array();
foreach ($fieldNames as $fieldName) {
if ($filter && !$filter->filter($fieldName)) {
continue;
}
$getter = 'get' . Inflector::classify($fieldName);
$isser = 'is' . Inflector::classify($fieldName);
$dataFieldName = $this->computeExtractFieldName($fieldName);
if (in_array($getter, $methods)) {
$data[$dataFieldName] = $this->extractValue($fieldName, $object->$getter(), $object);
} elseif (in_array($isser, $methods)) {
$data[$dataFieldName] = $this->extractValue($fieldName, $object->$isser(), $object);
} elseif (substr($fieldName, 0, 2) === 'is'
&& ctype_upper(substr($fieldName, 2, 1))
&& in_array($fieldName, $methods)) {
$data[$dataFieldName] = $this->extractValue($fieldName, $object->$fieldName(), $object);
}
// Unknown fields are ignored
}
return $data;
}
/**
* Extract values from an object using a by-reference logic (this means that values are
* directly fetched without using the public API of the entity, in this case, getters)
*
* @param object $object
* @return array
*/
protected function extractByReference($object)
{
$fieldNames = array_merge($this->metadata->getFieldNames(), $this->metadata->getAssociationNames());
$refl = $this->metadata->getReflectionClass();
$filter = $object instanceof FilterProviderInterface
? $object->getFilter()
: $this->filterComposite;
$data = array();
foreach ($fieldNames as $fieldName) {
if ($filter && !$filter->filter($fieldName)) {
continue;
}
$reflProperty = $refl->getProperty($fieldName);
$reflProperty->setAccessible(true);
$dataFieldName = $this->computeExtractFieldName($fieldName);
$data[$dataFieldName] = $this->extractValue($fieldName, $reflProperty->getValue($object), $object);
}
return $data;
}
/**
* Hydrate the object using a by-value logic (this means that it uses the entity API, in this
* case, setters)
*
* @param array $data
* @param object $object
* @throws RuntimeException
* @return object
*/
protected function hydrateByValue(array $data, $object)
{
$tryObject = $this->tryConvertArrayToObject($data, $object);
$metadata = $this->metadata;
if (is_object($tryObject)) {
$object = $tryObject;
}
foreach ($data as $field => $value) {
$field = $this->computeHydrateFieldName($field);
$value = $this->handleTypeConversions($value, $metadata->getTypeOfField($field));
$setter = 'set' . Inflector::classify($field);
if ($metadata->hasAssociation($field)) {
$target = $metadata->getAssociationTargetClass($field);
if ($metadata->isSingleValuedAssociation($field)) {
if (! method_exists($object, $setter)) {
continue;
}
$value = $this->toOne($target, $this->hydrateValue($field, $value, $data));
if (null === $value
&& !current($metadata->getReflectionClass()->getMethod($setter)->getParameters())->allowsNull()
) {
continue;
}
$object->$setter($value);
} elseif ($metadata->isCollectionValuedAssociation($field)) {
$this->toMany($object, $field, $target, $value);
}
} else {
if (! method_exists($object, $setter)) {
continue;
}
$object->$setter($this->hydrateValue($field, $value, $data));
}
}
return $object;
}
/**
* Hydrate the object using a by-reference logic (this means that values are modified directly without
* using the public API, in this case setters, and hence override any logic that could be done in those
* setters)
*
* @param array $data
* @param object $object
* @return object
*/
protected function hydrateByReference(array $data, $object)
{
$tryObject = $this->tryConvertArrayToObject($data, $object);
$metadata = $this->metadata;
$refl = $metadata->getReflectionClass();
if (is_object($tryObject)) {
$object = $tryObject;
}
foreach ($data as $field => $value) {
$field = $this->computeHydrateFieldName($field);
// Ignore unknown fields
if (!$refl->hasProperty($field)) {
continue;
}
$value = $this->handleTypeConversions($value, $metadata->getTypeOfField($field));
$reflProperty = $refl->getProperty($field);
$reflProperty->setAccessible(true);
if ($metadata->hasAssociation($field)) {
$target = $metadata->getAssociationTargetClass($field);
if ($metadata->isSingleValuedAssociation($field)) {
$value = $this->toOne($target, $this->hydrateValue($field, $value, $data));
$reflProperty->setValue($object, $value);
} elseif ($metadata->isCollectionValuedAssociation($field)) {
$this->toMany($object, $field, $target, $value);
}
} else {
$reflProperty->setValue($object, $this->hydrateValue($field, $value, $data));
}
}
return $object;
}
/**
* This function tries, given an array of data, to convert it to an object if the given array contains
* an identifier for the object. This is useful in a context of updating existing entities, without ugly
* tricks like setting manually the existing id directly into the entity
*
* @param array $data The data that may contain identifiers keys
* @param object $object
* @return object
*/
protected function tryConvertArrayToObject($data, $object)
{
$metadata = $this->metadata;
$identifierNames = $metadata->getIdentifierFieldNames($object);
$identifierValues = array();
if (empty($identifierNames)) {
return $object;
}
foreach ($identifierNames as $identifierName) {
if (!isset($data[$identifierName])) {
return $object;
}
$identifierValues[$identifierName] = $data[$identifierName];
}
return $this->find($identifierValues, $metadata->getName());
}
/**
* Handle ToOne associations
*
* When $value is an array but is not the $target's identifiers, $value is
* most likely an array of fieldset data. The identifiers will be determined
* and a target instance will be initialized and then hydrated. The hydrated
* target will be returned.
*
* @param string $target
* @param mixed $value
* @return object
*/
protected function toOne($target, $value)
{
$metadata = $this->objectManager->getClassMetadata($target);
if (is_array($value) && array_keys($value) != $metadata->getIdentifier()) {
// $value is most likely an array of fieldset data
$identifiers = array_intersect_key(
$value,
array_flip($metadata->getIdentifier())
);
$object = $this->find($identifiers, $target) ?: new $target;
return $this->hydrate($value, $object);
}
return $this->find($value, $target);
}
/**
* Handle ToMany associations. In proper Doctrine design, Collections should not be swapped, so
* collections are always handled by reference. Internally, every collection is handled using specials
* strategies that inherit from AbstractCollectionStrategy class, and that add or remove elements but without
* changing the collection of the object
*
* @param object $object
* @param mixed $collectionName
* @param string $target
* @param mixed $values
*
* @throws \InvalidArgumentException
*
* @return void
*/
protected function toMany($object, $collectionName, $target, $values)
{
$metadata = $this->objectManager->getClassMetadata(ltrim($target, '\\'));
$identifier = $metadata->getIdentifier();
if (!is_array($values) && !$values instanceof Traversable) {
$values = (array)$values;
}
$collection = array();
// If the collection contains identifiers, fetch the objects from database
foreach ($values as $value) {
if ($value instanceof $target) {
// assumes modifications have already taken place in object
$collection[] = $value;
continue;
} elseif (empty($value)) {
// assumes no id and retrieves new $target
$collection[] = $this->find($value, $target);
continue;
}
$find = array();
if (is_array($identifier)) {
foreach ($identifier as $field) {
switch (gettype($value)) {
case 'object':
$getter = 'get' . ucfirst($field);
if (method_exists($value, $getter)) {
$find[$field] = $value->$getter();
} elseif (property_exists($value, $field)) {
$find[$field] = $value->$field;
}
break;
case 'array':
if (array_key_exists($field, $value) && $value[$field] != null) {
$find[$field] = $value[$field];
unset($value[$field]); // removed identifier from persistable data
}
break;
default:
$find[$field] = $value;
break;
}
}
}
if (!empty($find) && $found = $this->find($find, $target)) {
$collection[] = (is_array($value)) ? $this->hydrate($value, $found) : $found;
} else {
$collection[] = (is_array($value)) ? $this->hydrate($value, new $target) : new $target;
}
}
$collection = array_filter(
$collection,
function ($item) {
return null !== $item;
}
);
// Set the object so that the strategy can extract the Collection from it
/** @var \DoctrineModule\Stdlib\Hydrator\Strategy\AbstractCollectionStrategy $collectionStrategy */
$collectionStrategy = $this->getStrategy($collectionName);
$collectionStrategy->setObject($object);
// We could directly call hydrate method from the strategy, but if people want to override
// hydrateValue function, they can do it and do their own stuff
$this->hydrateValue($collectionName, $collection, $values);
}
/**
* Handle various type conversions that should be supported natively by Doctrine (like DateTime)
*
* @param mixed $value
* @param string $typeOfField
* @return DateTime
*/
protected function handleTypeConversions($value, $typeOfField)
{
switch ($typeOfField) {
case 'datetimetz':
case 'datetime':
case 'time':
case 'date':
if ('' === $value) {
return null;
}
if (is_int($value)) {
$dateTime = new DateTime();
$dateTime->setTimestamp($value);
$value = $dateTime;
} elseif (is_string($value)) {
$value = new DateTime($value);
}
break;
default:
}
return $value;
}
/**
* Find an object by a given target class and identifier
*
* @param mixed $identifiers
* @param string $targetClass
*
* @return object|null
*/
protected function find($identifiers, $targetClass)
{
if ($identifiers instanceof $targetClass) {
return $identifiers;
}
if ($this->isNullIdentifier($identifiers)) {
return null;
}
return $this->objectManager->find($targetClass, $identifiers);
}
/**
* Verifies if a provided identifier is to be considered null
*
* @param mixed $identifier
*
* @return bool
*/
private function isNullIdentifier($identifier)
{
if (null === $identifier) {
return true;
}
if ($identifier instanceof Traversable || is_array($identifier)) {
$nonNullIdentifiers = array_filter(
ArrayUtils::iteratorToArray($identifier),
function ($value) {
return null !== $value;
}
);
return empty($nonNullIdentifiers);
}
return false;
}
/**
* Applies the naming strategy if there is one set
*
* @param string $field
*
* @return string
*/
protected function computeHydrateFieldName($field)
{
if ($this->hasNamingStrategy()) {
$field = $this->getNamingStrategy()->hydrate($field);
}
return $field;
}
/**
* Applies the naming strategy if there is one set
*
* @param string $field
*
* @return string
*/
protected function computeExtractFieldName($field)
{
if ($this->hasNamingStrategy()) {
$field = $this->getNamingStrategy()->extract($field);
}
return $field;
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Hydrator;
use Interop\Container\ContainerInterface;
class DoctrineObjectFactory
{
public function __invoke(ContainerInterface $container)
{
$em = $container->get('doctrine.entity_manager.orm_default');
return new DoctrineObject($em);
}
}

View File

@ -0,0 +1,66 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace App\Hydrator\Filter;
use Zend\Hydrator\Filter\FilterInterface;
/**
* Provides a filter to restrict returned fields by whitelisting or
* blacklisting property names.
*
* @license MIT
* @link http://www.doctrine-project.org/
* @author Liam O'Boyle <liam@ontheroad.net.nz>
*/
class PropertyName implements FilterInterface
{
/**
* The propteries to exclude.
*
* @var array
*/
protected $properties = array();
/**
* Either an exclude or an include.
*
* @var bool
*/
protected $exclude = null;
/**
* @param [ string | array ] $properties The properties to exclude or include.
* @param bool $exclude If the method should be excluded
*/
public function __construct($properties, $exclude = true)
{
$this->exclude = $exclude;
$this->properties = is_array($properties)
? $properties
: array($properties);
}
public function filter($property)
{
return in_array($property, $this->properties)
? !$this->exclude
: $this->exclude;
}
}

View File

@ -0,0 +1,190 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace App\Hydrator\Strategy;
use InvalidArgumentException;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Zend\Hydrator\Strategy\StrategyInterface;
/**
* @license MIT
* @link http://www.doctrine-project.org/
* @since 0.7.0
* @author Michael Gallego <mic.gallego@gmail.com>
*/
abstract class AbstractCollectionStrategy implements StrategyInterface
{
/**
* @var string
*/
protected $collectionName;
/**
* @var ClassMetadata
*/
protected $metadata;
/**
* @var object
*/
protected $object;
/**
* Set the name of the collection
*
* @param string $collectionName
* @return AbstractCollectionStrategy
*/
public function setCollectionName($collectionName)
{
$this->collectionName = (string) $collectionName;
return $this;
}
/**
* Get the name of the collection
*
* @return string
*/
public function getCollectionName()
{
return $this->collectionName;
}
/**
* Set the class metadata
*
* @param ClassMetadata $classMetadata
* @return AbstractCollectionStrategy
*/
public function setClassMetadata(ClassMetadata $classMetadata)
{
$this->metadata = $classMetadata;
return $this;
}
/**
* Get the class metadata
*
* @return ClassMetadata
*/
public function getClassMetadata()
{
return $this->metadata;
}
/**
* Set the object
*
* @param object $object
*
* @throws \InvalidArgumentException
*
* @return AbstractCollectionStrategy
*/
public function setObject($object)
{
if (!is_object($object)) {
throw new InvalidArgumentException(
sprintf('The parameter given to setObject method of %s class is not an object', get_called_class())
);
}
$this->object = $object;
return $this;
}
/**
* Get the object
*
* @return object
*/
public function getObject()
{
return $this->object;
}
/**
* {@inheritDoc}
*/
public function extract($value)
{
return $value;
}
/**
* Return the collection by value (using the public API)
*
* @throws \InvalidArgumentException
*
* @return Collection
*/
protected function getCollectionFromObjectByValue()
{
$object = $this->getObject();
$getter = 'get' . ucfirst($this->getCollectionName());
if (!method_exists($object, $getter)) {
throw new InvalidArgumentException(
sprintf(
'The getter %s to access collection %s in object %s does not exist',
$getter,
$this->getCollectionName(),
get_class($object)
)
);
}
return $object->$getter();
}
/**
* Return the collection by reference (not using the public API)
*
* @return Collection
*/
protected function getCollectionFromObjectByReference()
{
$object = $this->getObject();
$refl = $this->getClassMetadata()->getReflectionClass();
$reflProperty = $refl->getProperty($this->getCollectionName());
$reflProperty->setAccessible(true);
return $reflProperty->getValue($object);
}
/**
* This method is used internally by array_udiff to check if two objects are equal, according to their
* SPL hash. This is needed because the native array_diff only compare strings
*
* @param object $a
* @param object $b
*
* @return int
*/
protected function compareObjects($a, $b)
{
return strcmp(spl_object_hash($a), spl_object_hash($b));
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace App\Hydrator\Strategy;
/**
* When this strategy is used for Collections, if the new collection does not contain elements that are present in
* the original collection, then this strategy remove elements from the original collection. For instance, if the
* collection initially contains elements A and B, and that the new collection contains elements B and C, then the
* final collection will contain elements B and C (while element A will be asked to be removed).
*
* This strategy is by reference, this means it won't use public API to add/remove elements to the collection
*
* @license MIT
* @link http://www.doctrine-project.org/
* @since 0.7.0
* @author Michael Gallego <mic.gallego@gmail.com>
*/
class AllowRemoveByReference extends AbstractCollectionStrategy
{
/**
* {@inheritDoc}
*/
public function hydrate($value)
{
$collection = $this->getCollectionFromObjectByReference();
$collectionArray = $collection->toArray();
$toAdd = array_udiff($value, $collectionArray, array($this, 'compareObjects'));
$toRemove = array_udiff($collectionArray, $value, array($this, 'compareObjects'));
foreach ($toAdd as $element) {
$collection->add($element);
}
foreach ($toRemove as $element) {
$collection->removeElement($element);
}
return $collection;
}
}

View File

@ -0,0 +1,76 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace App\Hydrator\Strategy;
use Doctrine\Common\Collections\Collection;
use LogicException;
use Doctrine\Common\Collections\ArrayCollection;
/**
* When this strategy is used for Collections, if the new collection does not contain elements that are present in
* the original collection, then this strategy remove elements from the original collection. For instance, if the
* collection initially contains elements A and B, and that the new collection contains elements B and C, then the
* final collection will contain elements B and C (while element A will be asked to be removed).
*
* This strategy is by value, this means it will use the public API (in this case, adder and remover)
*
* @license MIT
* @link http://www.doctrine-project.org/
* @since 0.7.0
* @author Michael Gallego <mic.gallego@gmail.com>
*/
class AllowRemoveByValue extends AbstractCollectionStrategy
{
/**
* {@inheritDoc}
*/
public function hydrate($value)
{
// AllowRemove strategy need "adder" and "remover"
$adder = 'add' . ucfirst($this->collectionName);
$remover = 'remove' . ucfirst($this->collectionName);
if (!method_exists($this->object, $adder) || !method_exists($this->object, $remover)) {
throw new LogicException(
sprintf(
'AllowRemove strategy for DoctrineModule hydrator requires both %s and %s to be defined in %s
entity domain code, but one or both seem to be missing',
$adder,
$remover,
get_class($this->object)
)
);
}
$collection = $this->getCollectionFromObjectByValue();
if ($collection instanceof Collection) {
$collection = $collection->toArray();
}
$toAdd = new ArrayCollection(array_udiff($value, $collection, array($this, 'compareObjects')));
$toRemove = new ArrayCollection(array_udiff($collection, $value, array($this, 'compareObjects')));
$this->object->$adder($toAdd);
$this->object->$remover($toRemove);
return $collection;
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace App\Hydrator\Strategy;
/**
* When this strategy is used for Collections, if the new collection does not contain elements that are present in
* the original collection, then this strategy will not remove those elements. At most, it will add new elements. For
* instance, if the collection initially contains elements A and B, and that the new collection contains elements B
* and C, then the final collection will contain elements A, B and C.
*
* This strategy is by reference, this means it won't use the public API to remove elements
*
* @license MIT
* @link http://www.doctrine-project.org/
* @since 0.7.0
* @author Michael Gallego <mic.gallego@gmail.com>
*/
class DisallowRemoveByReference extends AbstractCollectionStrategy
{
/**
* {@inheritDoc}
*/
public function hydrate($value)
{
$collection = $this->getCollectionFromObjectByReference();
$collectionArray = $collection->toArray();
$toAdd = array_udiff($value, $collectionArray, array($this, 'compareObjects'));
foreach ($toAdd as $element) {
$collection->add($element);
}
return $collection;
}
}

View File

@ -0,0 +1,72 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace App\Hydrator\Strategy;
use Doctrine\Common\Collections\Collection;
use LogicException;
use Doctrine\Common\Collections\ArrayCollection;
/**
* When this strategy is used for Collections, if the new collection does not contain elements that are present in
* the original collection, then this strategy will not remove those elements. At most, it will add new elements. For
* instance, if the collection initially contains elements A and B, and that the new collection contains elements B
* and C, then the final collection will contain elements A, B and C.
*
* This strategy is by value, this means it will use the public API (in this case, remover)
*
* @license MIT
* @link http://www.doctrine-project.org/
* @since 0.7.0
* @author Michael Gallego <mic.gallego@gmail.com>
*/
class DisallowRemoveByValue extends AbstractCollectionStrategy
{
/**
* {@inheritDoc}
*/
public function hydrate($value)
{
// AllowRemove strategy need "adder"
$adder = 'add' . ucfirst($this->collectionName);
if (!method_exists($this->object, $adder)) {
throw new LogicException(
sprintf(
'DisallowRemove strategy for DoctrineModule hydrator requires %s to
be defined in %s entity domain code, but it seems to be missing',
$adder,
get_class($this->object)
)
);
}
$collection = $this->getCollectionFromObjectByValue();
if ($collection instanceof Collection) {
$collection = $collection->toArray();
}
$toAdd = new ArrayCollection(array_udiff($value, $collection, array($this, 'compareObjects')));
$this->object->$adder($toAdd);
return $collection;
}
}

0
templates/.gitkeep Normal file
View File

View File

@ -0,0 +1,28 @@
<?php
namespace AppTest\Action;
use App\Action\HomePageAction;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
use Zend\Expressive\Router\RouterInterface;
class HomePageActionTest extends \PHPUnit_Framework_TestCase
{
/** @var RouterInterface */
protected $router;
protected function setUp()
{
$this->router = $this->prophesize(RouterInterface::class);
}
public function testResponse()
{
$homePage = new HomePageAction($this->router->reveal(), null);
$response = $homePage(new ServerRequest(['/']), new Response(), function () {
});
$this->assertTrue($response instanceof Response);
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace AppTest\Action;
use App\Action\HomePageAction;
use App\Action\HomePageFactory;
use Interop\Container\ContainerInterface;
use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
class HomePageFactoryTest extends \PHPUnit_Framework_TestCase
{
/** @var ContainerInterface */
protected $container;
protected function setUp()
{
$this->container = $this->prophesize(ContainerInterface::class);
$router = $this->prophesize(RouterInterface::class);
$this->container->get(RouterInterface::class)->willReturn($router);
}
public function testFactoryWithoutTemplate()
{
$factory = new HomePageFactory();
$this->container->has(TemplateRendererInterface::class)->willReturn(false);
$this->assertTrue($factory instanceof HomePageFactory);
$homePage = $factory($this->container->reveal());
$this->assertTrue($homePage instanceof HomePageAction);
}
public function testFactoryWithTemplate()
{
$factory = new HomePageFactory();
$this->container->has(TemplateRendererInterface::class)->willReturn(true);
$this->container
->get(TemplateRendererInterface::class)
->willReturn($this->prophesize(TemplateRendererInterface::class));
$this->assertTrue($factory instanceof HomePageFactory);
$homePage = $factory($this->container->reveal());
$this->assertTrue($homePage instanceof HomePageAction);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace AppTest\Action;
use App\Action\PingAction;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
class PingActionTest extends \PHPUnit_Framework_TestCase
{
public function testResponse()
{
$pingAction = new PingAction();
$response = $pingAction(new ServerRequest(['/']), new Response(), function () {
});
$json = json_decode((string) $response->getBody());
$this->assertTrue($response instanceof Response);
$this->assertTrue($response instanceof Response\JsonResponse);
$this->assertTrue(isset($json->ack));
}
}