* initial commit

This commit is contained in:
Danyi Dávid 2017-09-03 17:51:59 +02:00
commit 92eefa7a63
51 changed files with 6493 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.idea
composer.phar
clover.xml
coveralls-upload.json
phpunit.xml
vendor/

138
README.md Normal file
View File

@ -0,0 +1,138 @@
# 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)
[![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-expressive-skeleton/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-expressive-skeleton?branch=master)
*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 run --timeout=0 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 serve` spawns continues running
> as a background process, but on other systems halts when the timeout occurs.
>
> As such, we recommend running the `serve` script using a timeout. This can
> be done by using `composer run` to execute the `serve` script, with a
> `--timeout` option. When set to `0`, as in the previous example, no timeout
> will be used, and it will run until you cancel the process (usually via
> `Ctrl-C`). Alternately, you can specify a finite timeout; as an example,
> the following will extend the timeout to a full day:
>
> ```bash
> $ composer run --timeout=86400 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).
## Application Development Mode Tool
This skeleton comes with [zf-development-mode](https://github.com/zfcampus/zf-development-mode).
It provides a composer script to allow you to enable and disable development mode.
### To enable development mode
**Note:** Do NOT run development mode on your production server!
```bash
$ composer development-enable
```
**Note:** Enabling development mode will also clear your configuration cache, to
allow safely updating dependencies and ensuring any new configuration is picked
up by your application.
### To disable development mode
```bash
$ composer development-disable
```
### Development mode status
```bash
$ composer development-status
```
## Configuration caching
By default, the skeleton will create a configuration cache in
`data/config-cache.php`. When in development mode, the configuration cache is
disabled, and switching in and out of development mode will remove the
configuration cache.
You may need to clear the configuration cache in production when deploying if
you deploy to the same directory. You may do so using the following:
```bash
$ composer clear-config-cache
```
You may also change the location of the configuration cache itself by editing
the `config/config.php` file and changing the `config_cache_path` entry of the
local `$cacheConfig` variable.
## 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 update --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).

View File

@ -0,0 +1,46 @@
<?php
/**
* Script for clearing the configuration cache.
*
* Can also be invoked as `composer clear-config-cache`.
*
* @see https://github.com/zendframework/zend-expressive-skeleton for the canonical source repository
* @copyright Copyright (c) 2017 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-expressive-skeleton/blob/master/LICENSE.md New BSD License
*/
chdir(__DIR__ . '/../');
require 'vendor/autoload.php';
$config = include 'config/config.php';
if (! isset($config['config_cache_path'])) {
echo "No configuration cache path found" . PHP_EOL;
exit(0);
}
if (! file_exists($config['config_cache_path'])) {
printf(
"Configured config cache file '%s' not found%s",
$config['config_cache_path'],
PHP_EOL
);
exit(0);
}
if (false === unlink($config['config_cache_path'])) {
printf(
"Error removing config cache file '%s'%s",
$config['config_cache_path'],
PHP_EOL
);
exit(1);
}
printf(
"Removed configured config cache file '%s'%s",
$config['config_cache_path'],
PHP_EOL
);
exit(0);

60
composer.json Normal file
View File

@ -0,0 +1,60 @@
{
"name": "zendframework/zend-expressive-skeleton",
"description": "Zend expressive skeleton. Begin developing PSR-7 middleware applications in seconds!",
"type": "project",
"homepage": "https://github.com/zendframework/zend-expressive-skeleton",
"license": "BSD-3-Clause",
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": "^7.1",
"dasprid/container-interop-doctrine": "^1.0",
"roave/security-advisories": "dev-master",
"zendframework/zend-component-installer": "^1.0",
"zendframework/zend-config-aggregator": "^1.0",
"zendframework/zend-expressive": "^2.0.2",
"zendframework/zend-expressive-fastroute": "^2.0",
"zendframework/zend-expressive-helpers": "^4.0",
"zendframework/zend-json": "^3.0",
"zendframework/zend-servicemanager": "^3.3",
"zendframework/zend-stdlib": "^3.1"
},
"require-dev": {
"phpunit/phpunit": "^6.0.8 || ^5.7.15",
"squizlabs/php_codesniffer": "^2.8.1",
"zfcampus/zf-development-mode": "^3.1",
"filp/whoops": "^2.1.7"
},
"autoload": {
"psr-4": {
"App\\": "src/App/"
}
},
"autoload-dev": {
"psr-4": {
"AppTest\\": "test/AppTest/"
}
},
"scripts": {
"post-create-project-cmd": [
"@development-enable"
],
"development-disable": "zf-development-mode disable",
"development-enable": "zf-development-mode enable",
"development-status": "zf-development-mode status",
"check": [
"@cs-check",
"@test"
],
"clear-config-cache": "php bin/clear-config-cache.php",
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"serve": "php -S 0.0.0.0:8080 -t public public/index.php",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml",
"upload-coverage": "coveralls -v"
}
}

3595
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

1
config/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
development.config.php

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

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

View File

@ -0,0 +1,42 @@
<?php
use Zend\Expressive\Application;
use Zend\Expressive\Container;
use Zend\Expressive\Delegate;
use Zend\Expressive\Helper;
use Zend\Expressive\Middleware;
return [
// Provides application-wide services.
// We recommend using fully-qualified class names whenever possible as
// service names.
'dependencies' => [
// Use 'aliases' to alias a service name to another service. The
// key is the alias name, the value is the service to which it points.
'aliases' => [
'Zend\Expressive\Delegate\DefaultDelegate' => Delegate\NotFoundDelegate::class,
],
// 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 => Container\ApplicationFactory::class,
Delegate\NotFoundDelegate::class => Container\NotFoundDelegateFactory::class,
Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class,
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
Zend\Stratigility\Middleware\ErrorHandler::class => Container\ErrorHandlerFactory::class,
Middleware\ErrorResponseGenerator::class => Container\ErrorResponseGeneratorFactory::class,
Middleware\NotFoundHandler::class => Container\NotFoundHandlerFactory::class,
'doctrine.entity_manager.orm_default' => \ContainerInteropDoctrine\EntityManagerFactory::class,
'doctrine.hydrator' => \App\Hydrator\DoctrineObjectFactory::class,
],
],
];

View File

@ -0,0 +1,34 @@
<?php
/**
* Development-only configuration.
*
* Put settings you want enabled when under development mode in this file, and
* check it into your repository.
*
* Developers on your team will then automatically enable them by calling on
* `composer development-enable`.
*/
use Zend\Expressive\Container;
use Zend\Expressive\Middleware\ErrorResponseGenerator;
return [
'dependencies' => [
'invokables' => [
],
'factories' => [
ErrorResponseGenerator::class => Container\WhoopsErrorResponseGeneratorFactory::class,
'Zend\Expressive\Whoops' => Container\WhoopsFactory::class,
'Zend\Expressive\WhoopsPageHandler' => Container\WhoopsPageHandlerFactory::class,
],
],
'whoops' => [
'json_exceptions' => [
'display' => true,
'show_trace' => true,
'ajax_only' => true,
],
],
];

View File

@ -0,0 +1,68 @@
<?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',
],
],
'configuration' => [
'orm_default' => [
// 'datetime_functions' => [
// 'date' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'time' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'timestamp' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'convert_tz' => Oro\ORM\Query\AST\Functions\DateTime\ConvertTz::class,
// ],
'numeric_functions' => [
// 'timestampdiff' => Oro\ORM\Query\AST\Functions\Numeric\TimestampDiff::class,
// 'dayofyear' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'dayofmonth' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'dayofweek' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'week' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'day' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'hour' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'minute' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
'month' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'quarter' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'second' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
'year' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'sign' => Oro\ORM\Query\AST\Functions\Numeric\Sign::class,
// 'pow' => Oro\ORM\Query\AST\Functions\Numeric\Pow::class,
],
// 'string_functions' => [
// 'md5' => Oro\ORM\Query\AST\Functions\SimpleFunction::class,
// 'group_concat' => Oro\ORM\Query\AST\Functions\String\GroupConcat::class,
// 'cast' => Oro\ORM\Query\AST\Functions\Cast::class,
// 'concat_ws' => Oro\ORM\Query\AST\Functions\String\ConcatWs::class
// ]
// 'filters' => [
// 'soft-deleteable' => Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter::class,
// ],
],
],
'event_manager' => [
'orm_default' => [
'subscribers' => [
Gedmo\Timestampable\TimestampableListener::class,
// 'Gedmo\Tree\TreeListener',
// 'Gedmo\SoftDeleteable\SoftDeleteableListener',
// 'Gedmo\Translatable\TranslatableListener',
// 'Gedmo\Blameable\BlameableListener',
// 'Gedmo\Loggable\LoggableListener',
// 'Gedmo\Sortable\SortableListener',
// 'Gedmo\Sluggable\SluggableListener',
],
],
],
],
];

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,11 @@
<?php
/**
* Local configuration.
*
* Copy this file to `local.php` and change its settings as required.
* `local.php` is ignored by git and safe to use for local and sensitive data like usernames and passwords.
*/
return [
];

View File

@ -0,0 +1,12 @@
<?php
use Zend\Expressive\Router\FastRouteRouter;
use Zend\Expressive\Router\RouterInterface;
return [
'dependencies' => [
'invokables' => [
RouterInterface::class => FastRouteRouter::class,
],
],
];

View File

@ -0,0 +1,27 @@
<?php
use Zend\ConfigAggregator\ConfigAggregator;
return [
// Toggle the configuration cache. Set this to boolean false, or remove the
// directive, to disable configuration caching. Toggling development mode
// will also disable it by default; clear the configuration cache using
// `composer clear-config-cache`.
ConfigAggregator::ENABLE_CACHE => true,
// Enable debugging; typically used to provide debugging information within templates.
'debug' => false,
'zend-expressive' => [
// Enable programmatic pipeline: Any `middleware_pipeline` or `routes`
// configuration will be ignored when creating the `Application` instance.
'programmatic_pipeline' => true,
// Provide templates for the error handling middleware to use when
// generating responses.
'error_handler' => [
'template_404' => 'error::404',
'template_error' => 'error::error',
],
],
];

32
config/config.php Normal file
View File

@ -0,0 +1,32 @@
<?php
use Zend\ConfigAggregator\ArrayProvider;
use Zend\ConfigAggregator\ConfigAggregator;
use Zend\ConfigAggregator\PhpFileProvider;
// To enable or disable caching, set the `ConfigAggregator::ENABLE_CACHE` boolean in
// `config/autoload/local.php`.
$cacheConfig = [
'config_cache_path' => 'data/config-cache.php',
];
$aggregator = new ConfigAggregator([
// Include cache configuration
new ArrayProvider($cacheConfig),
// Default App module config
App\ConfigProvider::class,
// Load application config in a pre-defined order in such a way that local settings
// overwrite global settings. (Loaded as first to last):
// - `global.php`
// - `*.global.php`
// - `local.php`
// - `*.local.php`
new PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
// Load development config if it exists
new PhpFileProvider('config/development.config.php'),
], $cacheConfig['config_cache_path']);
return $aggregator->getMergedConfig();

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;

View File

@ -0,0 +1,29 @@
<?php
/**
* File required to allow enablement of development mode.
*
* For use with the zf-development-mode tool.
*
* Usage:
* $ composer development-disable
* $ composer development-enable
* $ composer development-status
*
* DO NOT MODIFY THIS FILE.
*
* Provide your own development-mode settings by editing the file
* `config/autoload/development.local.php.dist`.
*
* Because this file is aggregated last, it simply ensures:
*
* - The `debug` flag is _enabled_.
* - Configuration caching is _disabled_.
*/
use Zend\ConfigAggregator\ConfigAggregator;
return [
'debug' => true,
ConfigAggregator::ENABLE_CACHE => false,
];

57
config/pipeline.php Normal file
View File

@ -0,0 +1,57 @@
<?php
use Zend\Expressive\Helper\ServerUrlMiddleware;
use Zend\Expressive\Helper\UrlHelperMiddleware;
use Zend\Expressive\Middleware\ImplicitHeadMiddleware;
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware;
use Zend\Expressive\Middleware\NotFoundHandler;
use Zend\Stratigility\Middleware\ErrorHandler;
/**
* Setup middleware pipeline:
*/
// The error handler should be the first (most outer) middleware to catch
// all Exceptions.
$app->pipe(ErrorHandler::class);
$app->pipe(App\Middleware\PreFlightMiddleware::class);
//$app->pipe(LosMiddleware\BasePath\BasePath::class);
$app->pipe(ServerUrlMiddleware::class);
// Pipe more middleware here that you want to execute on every request:
// - bootstrapping
// - pre-conditions
// - modifications to outgoing responses
//
// Piped Middleware may be either callables or service names. Middleware may
// also be passed as an array; each item in the array must resolve to
// middleware eventually (i.e., callable or service name).
//
// Middleware can be attached to specific paths, allowing you to mix and match
// applications under a common domain. The handlers in each middleware
// attached this way will see a URI with the MATCHED PATH SEGMENT REMOVED!!!
//
// - $app->pipe('/api', $apiMiddleware);
// - $app->pipe('/docs', $apiDocMiddleware);
// - $app->pipe('/files', $filesMiddleware);
// Register the routing middleware in the middleware pipeline
$app->pipeRoutingMiddleware();
$app->pipe(ImplicitHeadMiddleware::class);
$app->pipe(ImplicitOptionsMiddleware::class);
$app->pipe(UrlHelperMiddleware::class);
// Add more middleware here that needs to introspect the routing results; this
// might include:
//
// - route-based authentication
// - route-based validation
// - etc.
// Register the dispatch middleware in the middleware pipeline
$app->pipeDispatchMiddleware();
// At this point, if no Response is return by any middleware, the
// NotFoundHandler kicks in; alternately, you can provide other fallback
// middleware to execute.
$app->pipe(NotFoundHandler::class);

31
config/routes.php Normal file
View File

@ -0,0 +1,31 @@
<?php
/**
* Setup routes with a single request method:
*
* $app->get('/', App\Action\HomePageAction::class, 'home');
* $app->post('/album', App\Action\AlbumCreateAction::class, 'album.create');
* $app->put('/album/:id', App\Action\AlbumUpdateAction::class, 'album.put');
* $app->patch('/album/:id', App\Action\AlbumUpdateAction::class, 'album.patch');
* $app->delete('/album/:id', App\Action\AlbumDeleteAction::class, 'album.delete');
*
* Or with multiple request methods:
*
* $app->route('/contact', App\Action\ContactAction::class, ['GET', 'POST', ...], 'contact');
*
* Or handling all request methods:
*
* $app->route('/contact', App\Action\ContactAction::class)->setName('contact');
*
* or:
*
* $app->route(
* '/contact',
* App\Action\ContactAction::class,
* Zend\Expressive\Router\Route::HTTP_METHOD_ANY,
* 'contact'
* );
*/
$app->get('/', App\Action\HomePageAction::class, 'home');
$app->get('/list', App\Action\PingAction::class, 'api.list');
$app->post('/store/{direction:sent|received}/{hashKey}', App\Action\StoreAction::class, 'api.store');

2
data/.gitignore vendored Normal file
View File

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

50
deploy.php Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace Deployer;
require 'recipe/common.php';
set('ssh_type', 'native');
set('ssh_multiplexing', true);
set('repository', 'https://gogs.ragnarok.yvan.hu/yvan/sms-store.git');
set('shared_dirs', [
// 'data/tmp',
]);
set('shared_files', [
'config/autoload/doctrine.local.php',
]);
set('writable_dirs', []);
set('keep_releases', 3);
set('default_stage', 'production');
server('prod', 'alfheim.ragnarok.yvan.hu', 22)
->stage('production')
->user('yvan')
->forwardAgent()
->set('php_service_name', 'php7.1-fpm')
->set('deploy_path', '/mnt/apps/sms-store/api');
desc('Restart PHP-FPM service');
task('php-fpm:restart', function () {
// The user must have rights for restart service
// /etc/sudoers: username ALL=NOPASSWD:/bin/systemctl restart php-fpm.service
run('sudo service {{php_service_name}} reload');
});
after('deploy:symlink', 'php-fpm:restart');
task('deploy', [
'deploy:prepare',
'deploy:lock',
'deploy:release',
'deploy:update_code',
'deploy:shared',
'deploy:writable',
'deploy:vendors',
'deploy:clear_paths',
'deploy:symlink',
'deploy:unlock',
'cleanup',
]);
after('deploy', 'success');

20
phpcs.xml.dist Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0"?>
<ruleset name="Expressive Skeleton coding standard">
<description>Expressive Skeleton 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>

17
phpunit.xml.dist Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="App\\Tests">
<directory>./test</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<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

29
public/index.php Normal file
View File

@ -0,0 +1,29 @@
<?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';
/**
* Self-called anonymous function that creates its own scope and keep the global namespace clean.
*/
call_user_func(function () {
/** @var \Interop\Container\ContainerInterface $container */
$container = require 'config/container.php';
/** @var \Zend\Expressive\Application $app */
$app = $container->get(\Zend\Expressive\Application::class);
// Import programmatic/declarative middleware pipeline and routing
// configuration statements
require 'config/pipeline.php';
require 'config/routes.php';
$app->run();
});

BIN
public/zf-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 B

View File

@ -0,0 +1,158 @@
<?php
namespace App\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Json\Json;
abstract class AbstractAction implements ServerMiddlewareInterface
{
const IDENTIFIER_NAME = 'id';
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
$requestMethod = strtoupper($request->getMethod());
$id = $request->getAttribute(static::IDENTIFIER_NAME);
switch ($requestMethod) {
case 'GET':
return isset($id)
? $this->get($request, $delegate)
: $this->getList($request, $delegate);
case 'POST':
return $this->create($request, $delegate);
case 'PUT':
return $this->update($request, $delegate);
case 'DELETE':
return isset($id)
? $this->delete($request, $delegate)
: $this->deleteList($request, $delegate);
case 'HEAD':
return $this->head($request, $delegate);
case 'OPTIONS':
return $this->options($request, $delegate);
case 'PATCH':
return $this->patch($request, $delegate);
default:
return $delegate->process($request);
}
}
public function get(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function getList(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function create(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function update(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function delete(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function deleteList(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function head(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function options(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
public function patch(ServerRequestInterface $request, DelegateInterface $delegate)
{
return $this->createResponse(['content' => 'Method not allowed'], 405);
}
final protected function createResponse($data, $status = 200)
{
return new JsonResponse($data, $status);
}
/**
*
* @param ServerRequestInterface $request
* @return array|object
*/
public function getRequestData(ServerRequestInterface $request)
{
$body = $request->getParsedBody();
if (!empty($body)) {
return $body;
}
return $this->parseRequestData(
$request->getBody()->getContents(),
$request->getHeaderLine('content-type')
);
}
/**
*
* @param string $input
* @param string $contentType
* @return mixed
*/
private function parseRequestData($input, $contentType)
{
$contentTypeParts = preg_split('/\s*[;,]\s*/', $contentType);
$parser = $this->returnParserContentType($contentTypeParts[0]);
return $parser($input);
}
/**
*
* @param string $contentType
* @return callable
*/
private function returnParserContentType(string $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 Json();
try {
return $jsonDecoder->decode($input, Json::TYPE_ARRAY);
} catch (\Exception $e) {
return false;
}
};
} elseif ($contentType === 'multipart/form-data') {
return function ($input) {
return $input;
};
}
return function ($input) {
return $input;
};
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Router;
use Zend\Expressive\Template;
use Zend\Expressive\Plates\PlatesRenderer;
use Zend\Expressive\Twig\TwigRenderer;
use Zend\Expressive\ZendView\ZendViewRenderer;
class HomePageAction implements ServerMiddlewareInterface
{
private $router;
private $template;
public function __construct(Router\RouterInterface $router, Template\TemplateRendererInterface $template = null)
{
$this->router = $router;
$this->template = $template;
}
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
if (! $this->template) {
return new JsonResponse([
'welcome' => 'Congratulations! You have installed the zend-expressive skeleton application.',
'docsUrl' => 'https://docs.zendframework.com/zend-expressive/',
]);
}
$data = [];
if ($this->router instanceof Router\AuraRouter) {
$data['routerName'] = 'Aura.Router';
$data['routerDocs'] = 'http://auraphp.com/packages/2.x/Router.html';
} elseif ($this->router instanceof Router\FastRouteRouter) {
$data['routerName'] = 'FastRoute';
$data['routerDocs'] = 'https://github.com/nikic/FastRoute';
} elseif ($this->router instanceof Router\ZendRouter) {
$data['routerName'] = 'Zend Router';
$data['routerDocs'] = 'https://docs.zendframework.com/zend-router/';
}
if ($this->template instanceof PlatesRenderer) {
$data['templateName'] = 'Plates';
$data['templateDocs'] = 'http://platesphp.com/';
} elseif ($this->template instanceof TwigRenderer) {
$data['templateName'] = 'Twig';
$data['templateDocs'] = 'http://twig.sensiolabs.org/documentation';
} elseif ($this->template instanceof ZendViewRenderer) {
$data['templateName'] = 'Zend View';
$data['templateDocs'] = 'https://docs.zendframework.com/zend-view/';
}
return new HtmlResponse($this->template->render('app::home-page', $data));
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace App\Action;
use Interop\Container\ContainerInterface;
use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
class HomePageFactory
{
public function __invoke(ContainerInterface $container)
{
$router = $container->get(RouterInterface::class);
$template = $container->has(TemplateRendererInterface::class)
? $container->get(TemplateRendererInterface::class)
: null;
return new HomePageAction($router, $template);
}
}

View File

@ -0,0 +1,16 @@
<?php
namespace App\Action;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Zend\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ServerRequestInterface;
class PingAction implements ServerMiddlewareInterface
{
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
return new JsonResponse(['ack' => time()]);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Action;
use App\Entity\Sms;
use App\Response\JsonCorsResponse;
use App\Service\SmsStoreService;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Psr\Http\Message\ServerRequestInterface;
class StoreAction extends AbstractAction
{
private $smsStore;
public function __construct(SmsStoreService $smsStore)
{
$this->smsStore = $smsStore;
}
public function create(ServerRequestInterface $request, DelegateInterface $delegate)
{
$hashKey = $request->getAttribute('hashKey');
$direction = $request->getAttribute('direction','received');
$mappedDirection = Sms::MAP_DIRECTIONS[$direction];
$requestData = $this->getRequestData($request);
$isStored = $this->smsStore->storeSms($hashKey, $mappedDirection, $requestData);
return new JsonCorsResponse($isStored);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Action;
use App\Service\SmsStoreService;
use Interop\Container\ContainerInterface;
class StoreFactory
{
public function __invoke(ContainerInterface $container)
{
$smsStore = $container->get(SmsStoreService::class);
return new StoreAction($smsStore);
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App;
/**
* The configuration provider for the App module
*
* @see https://docs.zendframework.com/zend-component-installer/
*/
class ConfigProvider
{
/**
* Returns the configuration array
*
* To add a bit of a structure, each section is defined in a separate
* method which returns an array with its configuration.
*
* @return array
*/
public function __invoke()
{
return [
'dependencies' => $this->getDependencies(),
'templates' => $this->getTemplates(),
];
}
/**
* Returns the container dependencies
*
* @return array
*/
public function getDependencies()
{
return [
'invokables' => [
Action\PingAction::class => Action\PingAction::class,
],
'factories' => [
Action\HomePageAction::class => Action\HomePageFactory::class,
Service\SmsStoreService::class => Service\SmsStoreServiceFactory::class,
],
];
}
/**
* Returns the templates configuration
*
* @return array
*/
public function getTemplates()
{
return [
'paths' => [
'app' => ['templates/app'],
'error' => ['templates/error'],
'layout' => ['templates/layout'],
],
];
}
}

222
src/App/Entity/Sms.php Normal file
View File

@ -0,0 +1,222 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(
* name="sms_store",
* indexes={
* @ORM\Index(name="contact_name", columns={"contact_name"}),
* @ORM\Index(name="contact_number", columns={"contact_number"}),
* @ORM\Index(name="when", columns={"when"}),
* @ORM\Index(name="direction", columns={"direction"})
* }
* )
* @ORM\HasLifecycleCallbacks
*/
class Sms implements \JsonSerializable
{
const DIRECTION_SENT = 0;
const DIRECTION_RECEIVED = 1;
const MAP_DIRECTIONS = [
'sent' => self::DIRECTION_SENT,
'received' => self::DIRECTION_RECEIVED,
];
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="IDENTITY")
* @var int
*/
private $id;
/**
* @ORM\Column(name="contact_name", type="string", length=200, nullable=false)
* @var string
*/
private $contactName;
/**
* @ORM\Column(name="contact_number", type="string", length=100, nullable=false)
* @var string
*/
private $contactNumber;
/**
* @ORM\Column(name="text", type="text", length=4096, nullable=false)
* @var string
*/
private $text;
/**
* @ORM\Column(name="when", type="datetime", nullable=false)
* @var \DateTime
*/
private $when;
/**
* @ORM\Column(name="direction", type="integer", length=255, nullable=false)
* @var int
*/
private $direction;
/**
* @ORM\JoinColumn(name="owner_id", nullable=false)
* @ORM\ManyToOne(targetEntity="User", inversedBy="smsMessages")
* @var User
*/
private $owner;
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @param int $id
* @return Sms
*/
public function setId(int $id): Sms
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
public function getContactName(): string
{
return $this->contactName;
}
/**
* @param string $contactName
* @return Sms
*/
public function setContactName(string $contactName): Sms
{
$this->contactName = $contactName;
return $this;
}
/**
* @return string
*/
public function getContactNumber(): string
{
return $this->contactNumber;
}
/**
* @param string $contactNumber
* @return Sms
*/
public function setContactNumber(string $contactNumber): Sms
{
$this->contactNumber = $contactNumber;
return $this;
}
/**
* @return string
*/
public function getText(): string
{
return $this->text;
}
/**
* @param string $text
* @return Sms
*/
public function setText(string $text): Sms
{
$this->text = $text;
return $this;
}
/**
* @return \DateTime
*/
public function getWhen(): \DateTime
{
return $this->when;
}
/**
* @param \DateTime $when
* @return Sms
*/
public function setWhen(\DateTime $when): Sms
{
$this->when = $when;
return $this;
}
/**
* @return int
*/
public function getDirection(): int
{
return $this->direction;
}
/**
* @param int $direction
* @return Sms
*/
public function setDirection(int $direction): Sms
{
$this->direction = $direction;
return $this;
}
/**
* @return User
*/
public function getOwner(): User
{
return $this->owner;
}
/**
* @param User $owner
* @return Sms
*/
public function setOwner(User $owner): Sms
{
$this->owner = $owner;
return $this;
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return [
'id' => $this->getId(),
'contactName' => $this->getContactName(),
'contactNumber' => $this->getContactNumber(),
'text' => $this->getText(),
'when' => $this->getWhen(),
'direction' => $this->getDirection(),
'owner' => $this->getOwner(),
];
}
}

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

@ -0,0 +1,155 @@
<?php
namespace App\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(
* name="user",
* indexes={
* @ORM\Index(name="contact_name", columns={"contact_name"}),
* @ORM\Index(name="contact_number", columns={"contact_number"}),
* @ORM\Index(name="when", columns={"when"}),
* @ORM\Index(name="direction", columns={"direction"})
* }
* )
*/
class User implements \JsonSerializable
{
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="IDENTITY")
* @var int
*/
private $id;
/**
* @ORM\Column(name="name", type="string", length=200, nullable=false)
* @var string
*/
private $name;
/**
* @ORM\Column(name="hash_key", type="string", length=100, nullable=false)
* @var string
*/
private $hashKey;
/**
* @ORM\OneToMany(targetEntity="Fault", mappedBy="institute")
* @var Sms[]|ArrayCollection
*/
private $smsMessages;
public function __construct()
{
$this->smsMessages = new ArrayCollection();
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @param int $id
* @return User
*/
public function setId(int $id): User
{
$this->id = $id;
return $this;
}
/**
* @return string
*/
public function getName(): string
{
return $this->name;
}
/**
* @param string $name
* @return User
*/
public function setName(string $name): User
{
$this->name = $name;
return $this;
}
/**
* @return string
*/
public function getHashKey(): string
{
return $this->hashKey;
}
/**
* @param string $hashKey
* @return User
*/
public function setHashKey(string $hashKey): User
{
$this->hashKey = $hashKey;
return $this;
}
/**
* @return Sms[]|ArrayCollection
*/
public function getSmsMessages(): ?ArrayCollection
{
return $this->smsMessages;
}
/**
* @param Sms[]|ArrayCollection $smsMessages
* @return User
*/
public function setSmsMessages(?ArrayCollection $smsMessages): User
{
$this->smsMessages = $smsMessages;
return $this;
}
public function addSmsMessage(Sms $sms): User
{
if(!$this->smsMessages->contains($sms)) {
$this->smsMessages->add($sms);
}
return $this;
}
public function removeSmsMessage(Sms $sms): User{
if($this->smsMessages->contains($sms)) {
$this->smsMessages->removeElement($sms);
}
return $this;
}
/**
* Specify data which should be serialized to JSON
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return [
'id' => $this->getId(),
'name' => $this->getName(),
// 'smsMessages' => $this->getSmsMessages()->getValues(),
];
}
}

View File

@ -0,0 +1,594 @@
<?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 = [];
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 = [];
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 = [];
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 = [];
// 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 = [];
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 = [];
/**
* 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
: [$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, [$this, 'compareObjects']);
$toRemove = array_udiff($collectionArray, $value, [$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, [$this, 'compareObjects']));
$toRemove = new ArrayCollection(array_udiff($collection, $value, [$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, [$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, [$this, 'compareObjects']));
$this->object->$adder($toAdd);
return $collection;
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class PreFlightMiddleware
{
const ALLOW_HEADERS = [
'DNT',
'X-CustomHeader',
'Keep-Alive',
'User-Agent',
'X-Requested-With',
'If-Modified-Since',
'Cache-Control',
'Content-Type',
'Authorization',
];
public function __invoke(RequestInterface $request, ResponseInterface $response, callable $next)
{
$requestMethod = strtoupper($request->getMethod());
if ($requestMethod == 'OPTIONS') {
return $response
->withHeader('Accept', 'OPTIONS,GET,POST,PUT,PATCH,DELETE')
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'OPTIONS,GET,POST,PUT,PATCH,DELETE')
->withHeader('Access-Control-Allow-Headers', implode(",", self::ALLOW_HEADERS));
}
return $next($request, $response);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Response;
use Zend\Diactoros\Response\JsonResponse;
class JsonCorsResponse extends JsonResponse
{
const ALLOW_HEADERS = [
'DNT',
'X-CustomHeader',
'Keep-Alive',
'User-Agent',
'X-Requested-With',
'If-Modified-Since',
'Cache-Control',
'Content-Type',
'Authorization',
];
public function __construct(
$data,
$status = 200,
array $headers = [],
$encodingOptions = self::DEFAULT_JSON_FLAGS
) {
$headers['Access-Control-Allow-Origin'] = '*';
$headers['Access-Control-Allow-Methods'] = 'OPTIONS,GET,POST,PUT,PATCH,DELETE';
$headers['Access-Control-Allow-Headers'] = implode(",", self::ALLOW_HEADERS);
parent::__construct($data, $status, $headers, $encodingOptions);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace App\Service;
use App\Entity\Sms;
use App\Entity\User;
use Doctrine\ORM\EntityManager;
class SmsStoreService
{
/**
* @var EntityManager
*/
private $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function storeSms(string $hashKey, int $direction, array $requestData): bool
{
$user = $this->ensureUserExists($hashKey);
$sms = new Sms();
$sms->setDirection($direction)
->setContactName($requestData['contactName'])
->setContactNumber($requestData['contactNumber'])
->setWhen($requestData['when'])
->setOwner($user)
->setText($requestData['text']);
$this->em->persist($sms);
$this->em->flush();
return true;
}
/**
* @param string $hashKey
* @return User
*/
private function ensureUserExists(string $hashKey): User
{
/** @var User $user */
$user = $this->em->getRepository(User::class)->findOneBy([
'hashKey' => $hashKey
]);
if($user === null) {
$user = new User();
$user->setHashKey($hashKey);
$this->em->persist($user);
$this->em->flush();
}
return $user;
}
}

View File

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

View File

@ -0,0 +1,52 @@
<?php
namespace AppTest\Action;
use App\Action\HomePageAction;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\JsonResponse;
use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
class HomePageActionTest extends TestCase
{
/** @var RouterInterface */
protected $router;
protected function setUp()
{
$this->router = $this->prophesize(RouterInterface::class);
}
public function testReturnsJsonResponseWhenNoTemplateRendererProvided()
{
$homePage = new HomePageAction($this->router->reveal(), null);
$response = $homePage->process(
$this->prophesize(ServerRequestInterface::class)->reveal(),
$this->prophesize(DelegateInterface::class)->reveal()
);
$this->assertInstanceOf(JsonResponse::class, $response);
}
public function testReturnsHtmlResponseWhenTemplateRendererProvided()
{
$renderer = $this->prophesize(TemplateRendererInterface::class);
$renderer
->render('app::home-page', Argument::type('array'))
->willReturn('');
$homePage = new HomePageAction($this->router->reveal(), $renderer->reveal());
$response = $homePage->process(
$this->prophesize(ServerRequestInterface::class)->reveal(),
$this->prophesize(DelegateInterface::class)->reveal()
);
$this->assertInstanceOf(HtmlResponse::class, $response);
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace AppTest\Action;
use App\Action\HomePageAction;
use App\Action\HomePageFactory;
use Interop\Container\ContainerInterface;
use PHPUnit\Framework\TestCase;
use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
class HomePageFactoryTest extends 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->assertInstanceOf(HomePageFactory::class, $factory);
$homePage = $factory($this->container->reveal());
$this->assertInstanceOf(HomePageAction::class, $homePage);
}
public function testFactoryWithTemplate()
{
$factory = new HomePageFactory();
$this->container->has(TemplateRendererInterface::class)->willReturn(true);
$this->container
->get(TemplateRendererInterface::class)
->willReturn($this->prophesize(TemplateRendererInterface::class));
$this->assertInstanceOf(HomePageFactory::class, $factory);
$homePage = $factory($this->container->reveal());
$this->assertInstanceOf(HomePageAction::class, $homePage);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace AppTest\Action;
use App\Action\PingAction;
use Interop\Http\ServerMiddleware\DelegateInterface;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response\JsonResponse;
class PingActionTest extends TestCase
{
public function testResponse()
{
$pingAction = new PingAction();
$response = $pingAction->process(
$this->prophesize(ServerRequestInterface::class)->reveal(),
$this->prophesize(DelegateInterface::class)->reveal()
);
$json = json_decode((string) $response->getBody());
$this->assertInstanceOf(JsonResponse::class, $response);
$this->assertTrue(isset($json->ack));
}
}