diff --git a/composer.json b/composer.json index 5c6c8c0..88dfd9e 100644 --- a/composer.json +++ b/composer.json @@ -45,6 +45,7 @@ "league/commonmark": "^0.17.5", "los/loslog": "^3.1", "roave/security-advisories": "dev-master", + "tuupola/cors-middleware": "^0.7.0", "zendframework/zend-component-installer": "^2.1.1", "zendframework/zend-config-aggregator": "^1.0", "zendframework/zend-diactoros": "^1.7.1", @@ -52,6 +53,8 @@ "zendframework/zend-expressive-fastroute": "^3.0", "zendframework/zend-expressive-helpers": "^5.0", "zendframework/zend-expressive-platesrenderer": "^2.0", + "zendframework/zend-hydrator": "^2.4", + "zendframework/zend-json": "^3.1", "zendframework/zend-servicemanager": "^3.3", "zendframework/zend-stdlib": "^3.1" }, @@ -64,7 +67,8 @@ }, "autoload": { "psr-4": { - "App\\": "src/App/" + "App\\": "src/App/", + "DoctrineExpressiveModule\\": "src/DoctrineExpressiveModule/" } }, "autoload-dev": { diff --git a/composer.lock b/composer.lock index e70ae32..7048ea0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "5e320134b1fd52af2d7ed879f51b3602", + "content-hash": "d431f9aedef0713895d247778391bea1", "packages": [ { "name": "behat/transliterator", @@ -865,6 +865,58 @@ ], "time": "2018-04-13T13:49:18+00:00" }, + { + "name": "http-interop/http-factory", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/http-interop/http-factory.git", + "reference": "c2587cc0a6f74987fefb5b8074acfd32c69a4b0f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/http-interop/http-factory/zipball/c2587cc0a6f74987fefb5b8074acfd32c69a4b0f", + "reference": "c2587cc0a6f74987fefb5b8074acfd32c69a4b0f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/http-message": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Interop\\Http\\Factory\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "time": "2017-03-24T14:48:51+00:00" + }, { "name": "knplabs/knp-menu", "version": "2.3.0", @@ -1121,6 +1173,61 @@ ], "time": "2018-03-16T13:02:56+00:00" }, + { + "name": "neomerx/cors-psr7", + "version": "v1.0.12", + "source": { + "type": "git", + "url": "https://github.com/neomerx/cors-psr7.git", + "reference": "24944f39483d1a89f66ae9d58cca9f82b8815b35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/neomerx/cors-psr7/zipball/24944f39483d1a89f66ae9d58cca9f82b8815b35", + "reference": "24944f39483d1a89f66ae9d58cca9f82b8815b35", + "shasum": "" + }, + "require": { + "php": ">=5.6.0", + "psr/http-message": "^1.0", + "psr/log": "^1.0" + }, + "require-dev": { + "mockery/mockery": "^0.9.9", + "phpunit/phpunit": "^5.7", + "scrutinizer/ocular": "^1.1", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Neomerx\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "neomerx", + "email": "info@neomerx.com" + } + ], + "description": "Framework agnostic (PSR-7) CORS implementation (www.w3.org/TR/cors/)", + "homepage": "https://github.com/neomerx/cors-psr7", + "keywords": [ + "Cross Origin Resource Sharing", + "Cross-Origin Resource Sharing", + "cors", + "neomerx", + "psr-7", + "psr7", + "w3.org", + "www.w3.org" + ], + "time": "2017-09-03T22:31:57+00:00" + }, { "name": "nikic/fast-route", "version": "v1.3.0", @@ -1709,6 +1816,166 @@ ], "time": "2018-04-26T10:06:28+00:00" }, + { + "name": "tuupola/callable-handler", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/tuupola/callable-handler.git", + "reference": "5141efa1e974687a3fa53338811a988198f50662" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tuupola/callable-handler/zipball/5141efa1e974687a3fa53338811a988198f50662", + "reference": "5141efa1e974687a3fa53338811a988198f50662", + "shasum": "" + }, + "require": { + "php": "^7.0", + "psr/http-server-middleware": "^1.0" + }, + "require-dev": { + "codedungeon/phpunit-result-printer": "^0.4.4", + "overtrue/phplint": "^1.0", + "phpunit/phpunit": "^6.5", + "squizlabs/php_codesniffer": "^3.2", + "tuupola/http-factory": "^0.3.0", + "zendframework/zend-diactoros": "^1.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tuupola\\Middleware\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mika Tuupola", + "email": "tuupola@appelsiini.net", + "homepage": "https://appelsiini.net/", + "role": "Developer" + } + ], + "description": "Compatibility layer for PSR-7 double pass and PSR-15 middlewares.", + "homepage": "https://github.com/tuupola/callable-handler", + "keywords": [ + "middleware", + "psr-15", + "psr-7" + ], + "time": "2018-01-23T04:07:25+00:00" + }, + { + "name": "tuupola/cors-middleware", + "version": "0.7.0", + "source": { + "type": "git", + "url": "https://github.com/tuupola/cors-middleware.git", + "reference": "b0e2b7acacf22acae6ba029ee424fd6c073bb443" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tuupola/cors-middleware/zipball/b0e2b7acacf22acae6ba029ee424fd6c073bb443", + "reference": "b0e2b7acacf22acae6ba029ee424fd6c073bb443", + "shasum": "" + }, + "require": { + "neomerx/cors-psr7": "^1.0", + "php": "^7.1", + "psr/http-server-middleware": "^1.0", + "tuupola/callable-handler": "^0.3.0", + "tuupola/http-factory": "^0.3.0" + }, + "require-dev": { + "codedungeon/phpunit-result-printer": "^0.4.4", + "equip/dispatch": "dev-approved-psr15 as 1.0.x-dev", + "overtrue/phplint": "^1.0", + "phpunit/phpunit": "^6.5", + "squizlabs/php_codesniffer": "^3.2", + "zendframework/zend-diactoros": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tuupola\\Middleware\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mika Tuupola", + "email": "tuupola@appelsiini.net", + "homepage": "http://www.appelsiini.net/", + "role": "Developer" + } + ], + "description": "PSR-7 and PSR-15 CORS middleware", + "homepage": "https://github.com/tuupola/cors-middleware", + "keywords": [ + "cors", + "middleware", + "psr-15", + "psr-7" + ], + "time": "2018-01-25T02:29:07+00:00" + }, + { + "name": "tuupola/http-factory", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/tuupola/http-factory.git", + "reference": "57b2e19ff3f4af0bbee4e31fd282689be351f1ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tuupola/http-factory/zipball/57b2e19ff3f4af0bbee4e31fd282689be351f1ad", + "reference": "57b2e19ff3f4af0bbee4e31fd282689be351f1ad", + "shasum": "" + }, + "require": { + "http-interop/http-factory": "^0.3.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.3.0", + "overtrue/phplint": "^0.2.1", + "phpunit/phpunit": "^5.7", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Tuupola\\Http\\Factory\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mika Tuupola", + "email": "tuupola@appelsiini.net", + "homepage": "http://www.appelsiini.net/", + "role": "Developer" + } + ], + "description": "Lightweight autodiscovering PSR-17 HTTP factories", + "homepage": "https://github.com/tuupola/http-factory", + "keywords": [ + "http", + "psr-17", + "psr-7" + ], + "time": "2017-07-15T22:03:15+00:00" + }, { "name": "zendframework/zend-component-installer", "version": "2.1.1", @@ -2366,6 +2633,119 @@ ], "time": "2018-02-21T20:33:02+00:00" }, + { + "name": "zendframework/zend-hydrator", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-hydrator.git", + "reference": "bd48bc3bc046df007a94125f868dd1aa1b73a813" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-hydrator/zipball/bd48bc3bc046df007a94125f868dd1aa1b73a813", + "reference": "bd48bc3bc046df007a94125f868dd1aa1b73a813", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0", + "zendframework/zend-stdlib": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-eventmanager": "^2.6.2 || ^3.0", + "zendframework/zend-filter": "^2.6", + "zendframework/zend-inputfilter": "^2.6", + "zendframework/zend-serializer": "^2.6.1", + "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3" + }, + "suggest": { + "zendframework/zend-eventmanager": "^2.6.2 || ^3.0, to support aggregate hydrator usage", + "zendframework/zend-filter": "^2.6, to support naming strategy hydrator usage", + "zendframework/zend-serializer": "^2.6.1, to use the SerializableStrategy", + "zendframework/zend-servicemanager": "^2.7.5 || ^3.0.3, to support hydrator plugin manager usage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-release-1.0": "1.0.x-dev", + "dev-release-1.1": "1.1.x-dev", + "dev-master": "2.4.x-dev", + "dev-develop": "2.5.x-dev" + }, + "zf": { + "component": "Zend\\Hydrator", + "config-provider": "Zend\\Hydrator\\ConfigProvider" + } + }, + "autoload": { + "psr-4": { + "Zend\\Hydrator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Serialize objects to arrays, and vice versa", + "keywords": [ + "ZendFramework", + "hydrator", + "zf" + ], + "time": "2018-04-30T21:22:14+00:00" + }, + { + "name": "zendframework/zend-json", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-json.git", + "reference": "4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-json/zipball/4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c", + "reference": "4dd940e8e6f32f1d36ea6b0677ea57c540c7c19c", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "zendframework/zend-coding-standard": "~1.0.0", + "zendframework/zend-stdlib": "^2.7.7 || ^3.1" + }, + "suggest": { + "zendframework/zend-json-server": "For implementing JSON-RPC servers", + "zendframework/zend-xml2json": "For converting XML documents to JSON" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1.x-dev", + "dev-develop": "3.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", + "keywords": [ + "ZendFramework", + "json", + "zf" + ], + "time": "2018-01-04T17:51:34+00:00" + }, { "name": "zendframework/zend-log", "version": "2.10.0", diff --git a/config/autoload/dependencies.global.php b/config/autoload/dependencies.global.php index a9ab249..479de17 100644 --- a/config/autoload/dependencies.global.php +++ b/config/autoload/dependencies.global.php @@ -21,6 +21,7 @@ return [ // Use 'factories' for services provided by callbacks/factory classes. 'factories' => [ // Fully\Qualified\ClassName::class => Fully\Qualified\FactoryName::class, + Tuupola\Middleware\CorsMiddleware::class => DoctrineExpressiveModule\Middleware\CorsMiddlewareFactory::class, ], ], ]; diff --git a/config/config.php b/config/config.php index 7fcd778..a4f3f20 100644 --- a/config/config.php +++ b/config/config.php @@ -13,6 +13,7 @@ $cacheConfig = [ ]; $aggregator = new ConfigAggregator([ + \Zend\Hydrator\ConfigProvider::class, \Zend\Log\ConfigProvider::class, \Zend\Expressive\Router\FastRouteRouter\ConfigProvider::class, \Zend\HttpHandlerRunner\ConfigProvider::class, @@ -25,6 +26,7 @@ $aggregator = new ConfigAggregator([ \Zend\Expressive\Router\ConfigProvider::class, // Default App module config + DoctrineExpressiveModule\ConfigProvider::class, App\ConfigProvider::class, // Load application config in a pre-defined order in such a way that local settings diff --git a/config/pipeline.php b/config/pipeline.php index cfe8f0b..b838955 100644 --- a/config/pipeline.php +++ b/config/pipeline.php @@ -3,6 +3,7 @@ declare(strict_types=1); use Psr\Container\ContainerInterface; +use Tuupola\Middleware\CorsMiddleware; use Zend\Expressive\Application; use Zend\Expressive\Handler\NotFoundHandler; use Zend\Expressive\Helper\ServerUrlMiddleware; @@ -53,7 +54,8 @@ return function (Application $app, MiddlewareFactory $factory, ContainerInterfac // Order here matters; the MethodNotAllowedMiddleware should be placed // after the Implicit*Middleware. $app->pipe(ImplicitHeadMiddleware::class); - $app->pipe(ImplicitOptionsMiddleware::class); +// $app->pipe(ImplicitOptionsMiddleware::class); + $app->pipe(CorsMiddleware::class); $app->pipe(MethodNotAllowedMiddleware::class); // Seed the UrlHelper with the routing results: diff --git a/config/routes.php b/config/routes.php index 65b6ea8..3d8cbce 100644 --- a/config/routes.php +++ b/config/routes.php @@ -36,6 +36,20 @@ return function (Application $app, MiddlewareFactory $factory, ContainerInterfac $app->get('/', App\Handler\HomePageHandler::class, 'home'); $app->get('/api/ping', App\Handler\PingHandler::class, 'api.ping'); + $app->get('/api/years', App\Handler\Api\YearsHandler::class, 'api.years'); + $app->route( + '/api/judge[/{id:\d+}]', + App\Handler\Api\JudgesHandler::class, + ['GET','POST','PUT','DELETE'], + 'api.judges' + ); + $app->route( + '/api/awardee[/{id:\d+}]', + App\Handler\Api\AwardeeHandler::class, + ['GET','POST','PUT','DELETE'], + 'api.awardees' + ); + $app->get('/the-prize', App\Handler\PrizeRedirectHandler::class, 'the-prize'); $app->get( '/the-prize/{article:background-and-purpose|description-and-values|aspects-for-selection|gran-prize-award-events}', diff --git a/src/App/ConfigProvider.php b/src/App/ConfigProvider.php index 110e817..c63c9c0 100644 --- a/src/App/ConfigProvider.php +++ b/src/App/ConfigProvider.php @@ -41,15 +41,19 @@ class ConfigProvider Handler\AwardeeHandler::class => Handler\AwardeeHandlerFactory::class, Handler\JudgesHandler::class => Handler\JudgesHandlerFactory::class, Handler\ProfileHandler::class => Handler\ProfileHandlerFactory::class, - Handler\AwardeeRedirectHandler::class => Handler\AwardeeRedirectHandlerFactory::class, Handler\PrizeRedirectHandler::class => Handler\PrizeRedirectHandlerFactory::class, + Handler\Api\AwardeeHandler::class => Handler\Api\AwardeeHandlerFactory::class, + Handler\Api\JudgesHandler::class => Handler\Api\JudgesHandlerFactory::class, + Handler\Api\YearsHandler::class => Handler\Api\YearsHandlerFactory::class, + Plates\StringExtension::class => Plates\StringExtensionFactory::class, Plates\NavigationExtension::class => Plates\NavigationExtensionFactory::class, Service\AwardeeManager::class => Service\AwardeeManagerFactory::class, Service\JudgeManager::class => Service\JudgeManagerFactory::class, + Service\YearManager::class => Service\YearManagerFactory::class, ], ]; } diff --git a/src/App/Entity/Awardee.php b/src/App/Entity/Awardee.php index e230b81..2899481 100644 --- a/src/App/Entity/Awardee.php +++ b/src/App/Entity/Awardee.php @@ -173,7 +173,7 @@ class Awardee implements JsonSerializable return [ 'id' => $this->getId(), 'year' => $this->getYear(), - 'name' => $this->getYear(), + 'name' => $this->getName(), 'text' => $this->getText(), 'imageLabel' => $this->getImageLabel(), 'slug' => $this->getSlug(), diff --git a/src/App/Entity/Judge.php b/src/App/Entity/Judge.php index 4887bd5..fff4e93 100644 --- a/src/App/Entity/Judge.php +++ b/src/App/Entity/Judge.php @@ -34,12 +34,6 @@ class Judge implements JsonSerializable */ private $name; - /** - * @ORM\Column(name="title", type="text", length=1500) - * @var string - */ - private $title; - /** * @ORM\Column(name="slug", type="string", length=250) * @Gedmo\Slug(fields={"name"}) @@ -48,14 +42,15 @@ class Judge implements JsonSerializable private $slug; /** - * @ORM\ManyToMany(targetEntity="Year", mappedBy="judges") - * @var Collection|Year[] + * @ORM\OneToMany(targetEntity="JudgeTitle", mappedBy="judge") + * @var Collection|JudgeTitle[] */ - private $years; + private $titles; + public function __construct() { - $this->years = new ArrayCollection(); + $this->titles = new ArrayCollection(); } /** @@ -94,24 +89,6 @@ class Judge implements JsonSerializable return $this; } - /** - * @return string - */ - public function getTitle(): string - { - return $this->title; - } - - /** - * @param string $title - * @return Judge - */ - public function setTitle(string $title): Judge - { - $this->title = $title; - return $this; - } - /** * @return string */ @@ -131,48 +108,44 @@ class Judge implements JsonSerializable } /** - * @return Year[]|Collection + * @return JudgeTitle[]|Collection */ - public function getYears(): ?Collection + public function getTitles() { - return $this->years; + return $this->titles; } /** - * @param Year $year + * @param JudgeTitle $title * @return Judge */ - public function addYear(Year $year): Judge + public function addTitle(JudgeTitle $title): Judge { - if ($this->years->contains($year)) { - return $this; + if(!$this->titles->contains($title)) { + $this->titles->add($title); } - $this->years->add($year); - $year->addJudge($this); return $this; } /** - * @param Year $year + * @param JudgeTitle $title * @return Judge */ - public function removeYear(Year $year): Judge + public function removeTitle(JudgeTitle $title): Judge { - if (!$this->years->contains($year)) { - return $this; + if($this->titles->contains($title)) { + $this->titles->removeElement($title); } - $this->years->removeElement($year); - $year->removeJudge($this); return $this; } /** - * @param Year[]|Collection $years + * @param JudgeTitle[]|Collection $titles * @return Judge */ - public function setYears(?Collection $years) + public function setTitles($titles) { - $this->years = $years; + $this->titles = $titles; return $this; } @@ -184,7 +157,7 @@ class Judge implements JsonSerializable return [ 'id' => $this->getId(), 'name' => $this->getName(), - 'title' => $this->getTitle(), + 'titles' => $this->getTitles()->getValues(), 'slug' => $this->getSlug(), ]; } diff --git a/src/App/Entity/JudgeTitle.php b/src/App/Entity/JudgeTitle.php new file mode 100644 index 0000000..812d1ec --- /dev/null +++ b/src/App/Entity/JudgeTitle.php @@ -0,0 +1,131 @@ +id; + } + + /** + * @param int $id + * @return JudgeTitle + */ + public function setId(int $id): JudgeTitle + { + $this->id = $id; + return $this; + } + + /** + * @return int + */ + public function getYear(): int + { + return $this->year; + } + + /** + * @param int $year + * @return JudgeTitle + */ + public function setYear(int $year): JudgeTitle + { + $this->year = $year; + return $this; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @param string $title + * @return JudgeTitle + */ + public function setTitle(string $title): JudgeTitle + { + $this->title = $title; + return $this; + } + + /** + * @return Judge + */ + public function getJudge(): Judge + { + return $this->judge; + } + + /** + * @param Judge $judge + * @return JudgeTitle + */ + public function setJudge(Judge $judge): JudgeTitle + { + $this->judge = $judge; + return $this; + } + + + /** + * @return array + */ + public function jsonSerialize() + { + return [ + 'id' => $this->getId(), + 'year' => $this->getYear(), + 'title' => $this->getTitle(), + ]; + } +} diff --git a/src/App/Entity/Year.php b/src/App/Entity/Year.php deleted file mode 100644 index 476bc91..0000000 --- a/src/App/Entity/Year.php +++ /dev/null @@ -1,149 +0,0 @@ -judges = new ArrayCollection(); - } - - /** - * @return int - */ - public function getId(): int - { - return $this->id; - } - - /** - * @param int $id - * @return Year - */ - public function setId(int $id): Year - { - $this->id = $id; - return $this; - } - - /** - * @return int - */ - public function getYear(): int - { - return $this->year; - } - - /** - * @param int $year - * @return Year - */ - public function setYear(int $year): Year - { - $this->year = $year; - return $this; - } - - /** - * @return Judge[]|Collection - */ - public function getJudges(): ?Collection - { - return $this->judges; - } - - /** - * @param Judge $judge - * @return Year - */ - public function addJudge(Judge $judge): Year - { - if ($this->judges->contains($judge)) { - return $this; - } - $this->judges->add($judge); - $judge->addYear($this); - return $this; - } - - /** - * @param Judge $judge - * @return Year - */ - public function removeJudge(Judge $judge): Year - { - if (!$this->judges->contains($judge)) { - return $this; - } - $this->judges->removeElement($judge); - $judge->removeYear($this); - return $this; - } - - /** - * @param Judge[]|Collection $judges - * @return Year - */ - public function setJudges(?Collection $judges): Year - { - $this->judges = $judges; - return $this; - } - - /** - * @return array - */ - public function jsonSerialize() - { - return [ - 'id' => $this->getId(), - 'year' => $this->getYear(), - 'judges' => $this->getJudges()->getValues(), - ]; - } -} diff --git a/src/App/Handler/Api/AbstractCrudHandler.php b/src/App/Handler/Api/AbstractCrudHandler.php new file mode 100644 index 0000000..eaece5a --- /dev/null +++ b/src/App/Handler/Api/AbstractCrudHandler.php @@ -0,0 +1,161 @@ +getMethod()); + $id = $request->getAttribute(static::IDENTIFIER_NAME); + + switch ($requestMethod) { + case 'GET': + return isset($id) + ? $this->get($request) + : $this->getList($request); + case 'POST': + return $this->create($request); + case 'PUT': + return $this->update($request); + case 'DELETE': + return isset($id) + ? $this->delete($request) + : $this->deleteList($request); + case 'HEAD': + return $this->head($request); + case 'OPTIONS': + return $this->options($request); + case 'PATCH': + return $this->patch($request); + default: + return $this->createResponse(['content' => 'Method not allowed'], 405); + } + } + + public function get(ServerRequestInterface $request) + { + return $this->createResponse(['content' => 'Method not allowed'], 405); + } + + public function getList(ServerRequestInterface $request) + { + return $this->createResponse(['content' => 'Method not allowed'], 405); + } + + public function create(ServerRequestInterface $request) + { + return $this->createResponse(['content' => 'Method not allowed'], 405); + } + + public function update(ServerRequestInterface $request) + { + return $this->createResponse(['content' => 'Method not allowed'], 405); + } + + public function delete(ServerRequestInterface $request) + { + return $this->createResponse(['content' => 'Method not allowed'], 405); + } + + public function deleteList(ServerRequestInterface $request) + { + return $this->createResponse(['content' => 'Method not allowed'], 405); + } + + public function head(ServerRequestInterface $request) + { + return $this->createResponse(['content' => 'Method not allowed'], 405); + } + + public function options(ServerRequestInterface $request) + { + return new EmptyResponse(200); + } + + public function patch(ServerRequestInterface $request) + { + 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): callable + { + 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; + }; + } +} diff --git a/src/App/Handler/Api/AwardeeHandler.php b/src/App/Handler/Api/AwardeeHandler.php new file mode 100644 index 0000000..912e2ab --- /dev/null +++ b/src/App/Handler/Api/AwardeeHandler.php @@ -0,0 +1,35 @@ +awardeeManager = $awardeeManager; + } + + public function getList(ServerRequestInterface $request): ResponseInterface + { + $entities = $this->awardeeManager->getAwardees(); + return new JsonResponse($entities); + } + + public function get(ServerRequestInterface $request): ResponseInterface + { + $id = $request->getAttribute(static::IDENTIFIER_NAME); + $entity = $this->awardeeManager->getAwardee((int)$id); + return new JsonResponse($entity); + } +} diff --git a/src/App/Handler/Api/AwardeeHandlerFactory.php b/src/App/Handler/Api/AwardeeHandlerFactory.php new file mode 100644 index 0000000..06d68b0 --- /dev/null +++ b/src/App/Handler/Api/AwardeeHandlerFactory.php @@ -0,0 +1,18 @@ +get(AwardeeManager::class); + return new AwardeeHandler($awardeeManager); + } +} diff --git a/src/App/Handler/Api/JudgesHandler.php b/src/App/Handler/Api/JudgesHandler.php new file mode 100644 index 0000000..5d2cab0 --- /dev/null +++ b/src/App/Handler/Api/JudgesHandler.php @@ -0,0 +1,70 @@ +judgeManager = $judgeManager; + } + + /** + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function getList(ServerRequestInterface $request): ResponseInterface + { + $entities = $this->judgeManager->getJudges(); + return new JsonResponse($entities); + } + + /** + * @param ServerRequestInterface $request + * @return JsonResponse + */ + public function get(ServerRequestInterface $request): ResponseInterface + { + $id = $request->getAttribute(static::IDENTIFIER_NAME); + $entity = $this->judgeManager->getJudge((int)$id); + return new JsonResponse($entity); + } + + /** + * @param ServerRequestInterface $request + * @return JsonResponse + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\OptimisticLockException + */ + public function create(ServerRequestInterface $request): ResponseInterface + { + $data = $this->getRequestData($request); + $entity = $this->judgeManager->create($data); + return new JsonResponse($entity); + } + + /** + * @param ServerRequestInterface $request + * @return JsonResponse + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\OptimisticLockException + */ + public function update(ServerRequestInterface $request): ResponseInterface + { + $id = $request->getAttribute(static::IDENTIFIER_NAME); + $data = $this->getRequestData($request); + $entity = $this->judgeManager->update((int)$id, $data); + return new JsonResponse($entity); + } +} diff --git a/src/App/Handler/Api/JudgesHandlerFactory.php b/src/App/Handler/Api/JudgesHandlerFactory.php new file mode 100644 index 0000000..939ddce --- /dev/null +++ b/src/App/Handler/Api/JudgesHandlerFactory.php @@ -0,0 +1,18 @@ +get(JudgeManager::class); + return new JudgesHandler($judgeManager); + } +} diff --git a/src/App/Handler/Api/YearsHandler.php b/src/App/Handler/Api/YearsHandler.php new file mode 100644 index 0000000..cafe9bb --- /dev/null +++ b/src/App/Handler/Api/YearsHandler.php @@ -0,0 +1,32 @@ +yearManager = $yearManager; + } + + /** + * @param ServerRequestInterface $request + * @return ResponseInterface + */ + public function getList(ServerRequestInterface $request) : ResponseInterface + { + $judges = $this->yearManager->getYears(); + return new JsonResponse($judges); + } +} diff --git a/src/App/Handler/Api/YearsHandlerFactory.php b/src/App/Handler/Api/YearsHandlerFactory.php new file mode 100644 index 0000000..36661a7 --- /dev/null +++ b/src/App/Handler/Api/YearsHandlerFactory.php @@ -0,0 +1,18 @@ +get(YearManager::class); + return new YearsHandler($yearManager); + } +} diff --git a/src/App/Handler/AwardeeHandler.php b/src/App/Handler/AwardeeHandler.php index 197ebc5..710049a 100644 --- a/src/App/Handler/AwardeeHandler.php +++ b/src/App/Handler/AwardeeHandler.php @@ -47,13 +47,6 @@ class AwardeeHandler implements RequestHandlerInterface $awardees = $this->awardeeManager->getAwardeesByYear((int)$year); $judges = $this->judgeManager->getJudgesByYear((int)$year); -// if (count($awardees) === 1) { -// $url = $this->urlHelper->generate('awardee', [ -// 'slug' => $awardees[0]->getSlug(), -// ]); -// return new RedirectResponse($url); -// } - return new HtmlResponse($this->template->render("app::awardees", [ 'year' => $year, 'awardees' => $awardees, diff --git a/src/App/Service/AwardeeManager.php b/src/App/Service/AwardeeManager.php index aa93729..2e27197 100644 --- a/src/App/Service/AwardeeManager.php +++ b/src/App/Service/AwardeeManager.php @@ -17,6 +17,13 @@ class AwardeeManager $this->entityManager = $entityManager; } + public function getAwardees(): ?array + { + return $this->entityManager->getRepository(Awardee::class)->findBy([], [ + 'year' => 'DESC', + ]); + } + /** * @param int $year * @return Awardee[] @@ -28,6 +35,13 @@ class AwardeeManager ]); } + public function getAwardee(int $id): ?Awardee + { + /** @var Awardee $awardee */ + $awardee = $this->entityManager->getRepository(Awardee::class)->find($id); + return $awardee; + } + /** * @param string $slug * @return Awardee|null diff --git a/src/App/Service/JudgeManager.php b/src/App/Service/JudgeManager.php index c579b9a..e296656 100644 --- a/src/App/Service/JudgeManager.php +++ b/src/App/Service/JudgeManager.php @@ -4,146 +4,89 @@ namespace App\Service; use App\Entity\Judge; use Doctrine\ORM\EntityManager; +use DoctrineExpressiveModule\Hydrator\DoctrineObject; class JudgeManager { /** @var EntityManager */ private $entityManager; - public function __construct(EntityManager $entityManager) - { + private $hydrator; + + public function __construct( + EntityManager $entityManager, + DoctrineObject $hydrator + ) { $this->entityManager = $entityManager; + $this->hydrator = $hydrator; } public function getJudges() { -// return [ -// [ -// 'image' => 'agnes_soos', -// 'name' => 'Dr Ágnes Soós', -// 'desc' => 'National Institute for Sports Medicine, Director General', -// ], [ -// 'image' => 'balazs_nagy_lantos', -// 'name' => 'Balázs Nagy Lantos', -// 'desc' => 'Mensa HungarIQa, Former President', -// ], [ -// 'image' => 'bertalan_mesko', -// 'name' => 'Dr Bertalan Meskó', -// 'desc' => 'Winner of GRAN PRIZE 2013, medical futurist, founder of Webicina', -// ], [ -// 'image' => 'edit_nemeth', -// 'name' => 'Dr Edit Németh', -// 'desc' => 'ELTE Institute of Business Economics, Management and Business Law Faculty', -// ], [ -// 'image' => 'erno_keszei', -// 'name' => 'Prof. Ernő Keszei', -// 'desc' => 'Eötvös Loránd University, Former Vice-Rector for Science, Research, and Innovation', -// ], [ -// 'image' => 'gabor_kopek', -// 'name' => 'Gábor Kopek', -// 'desc' => 'Moholy-Nagy University of Art and Design Budapest, Former Rector', -// ], [ -// 'image' => 'gabor_nemeth', -// 'name' => 'Dr Gábor Németh', -// 'desc' => 'Hungarian Intellectual Property Office, Director', -// ], [ -// 'image' => 'gabor_szabo', -// 'name' => 'Dr Gábor Szabó', -// 'desc' => 'University of Szeged, Rector; Hungarian Association for Innovation, President', -// ], [ -// 'image' => 'gyorgy_nagy', -// 'name' => 'György Nagy', -// 'desc' => 'Sigma Technology, Managing Director; Swedish Chamber of Commerce in Hungary, Vice-President', -// ], [ -// 'image' => 'gyula_patko', -// 'name' => 'Dr Gyula Patkó', -// 'desc' => 'University of Miskolc, Former Rector', -// ], [ -// 'image' => 'ildiko_csejtei', -// 'name' => 'Ildikó B. Csejtei', -// 'desc' => 'Independent Media Group, Owner, Director', -// ], [ -// 'image' => 'istvan_salgo', -// 'name' => 'István Salgó', -// 'desc' => 'Business Council for Sustainable Development in Hungary, Honorary President', -// ], [ -// 'image' => 'janos_durr', -// 'name' => 'János Dürr', -// 'desc' => 'Club of Hungarian Science Journalists, President', -// ], [ -// 'image' => 'janos_takacs', -// 'name' => 'János Takács', -// 'desc' => 'Swedish Chamber of Commerce, President', -// ], [ -// 'image' => 'jozsef_fulop', -// 'name' => 'József Fülöp', -// 'desc' => 'Moholy-Nagy University of Art and Design Budapest, Rector', -// ], [ -// 'image' => 'jozsef_peter_martin', -// 'name' => 'Dr József Péter Martin', -// 'desc' => 'Transparency International Hungary, Managing Director', -// ], [ -// 'image' => 'maria_judit_molnar', -// 'name' => 'Dr Mária Judit Molnár', -// 'desc' => 'SOTE Institute of Genomic Medicine and Rare Disorders, Head of the Institute', -// ], [ -// 'image' => 'melinda_kamasz', -// 'name' => 'Melinda Kamasz', -// 'desc' => 'Figyelő, Deputy Editor in Chief, Figyelő Trend, Editor in Chief', -// ], [ -// 'image' => 'miklos_antalovits', -// 'name' => 'Dr Miklós Antalovits', -// 'desc' => 'Budapest University of Technology and Economics, Professor Emeritus', -// ], [ -// 'image' => 'miklos_bendzsel', -// 'name' => 'Dr Miklós Bendzsel', -// 'desc' => 'Hungarian Academy of Engineers, Vice-President; Hungarian Intellectual Property Office, Former President', -// ], [ -// 'image' => 'peter_szauer', -// 'name' => 'Péter Szauer', -// 'desc' => 'HVG, President and Chief Executive Officer', -// ], [ -// 'image' => 'richard_bogdan', -// 'name' => 'Richárd Bogdán', -// 'desc' => 'Mensa HungarIQa, President', -// ], [ -// 'image' => 'rita_istivan', -// 'name' => 'Rita Istiván', -// 'desc' => 'BME, Honorary Associate Professor; Swedish Chamber of Commerce, Secretary General', -// ], [ -// 'image' => 'roland_jakab', -// 'name' => 'Roland Jakab', -// 'desc' => 'Ericsson Hungary, Managing Director; Swedish Chamber of Commerce, Member of the Board', -// ], [ -// 'image' => 'szabolcs_farkas', -// 'name' => 'Dr Szabolcs Farkas', -// 'desc' => 'Hungarian Intellectual Property Office, Vice-President', -// ], [ -// 'image' => 'zoltan_bruckner', -// 'name' => 'Zoltán Bruckner', -// 'desc' => 'Primus Capital Management, Investment Director, Managing Partner', -// ], [ -// 'image' => 'viktor_luszcz', -// 'name' => 'Dr Viktor Łuszcz', -// 'desc' => 'Hungarian Intellectual Property Office, President', -// ] -// ]; + $qb = $this->entityManager->createQueryBuilder(); + return $qb->select('j, t') + ->from(Judge::class, 'j') + ->leftJoin('j.titles', 't') + ->getQuery() + ->getArrayResult(); } /** * @param int $year * @return array - * @todo implement real filter by year */ public function getJudgesByYear(int $year) { $qb = $this->entityManager->createQueryBuilder(); return $qb->select('j') ->from(Judge::class, 'j') - ->innerJoin('j.years', 'y') - ->where('y.year = :year') + ->innerJoin('j.titles', 't') + ->where('t.year = :year') ->setParameter('year', $year) ->getQuery() ->getArrayResult(); } + + /** + * @param int $id + * @return Judge|null + */ + public function getJudge(int $id): ?Judge + { + /** @var Judge $judge */ + $judge = $this->entityManager->getRepository(Judge::class)->find($id); + return $judge; + } + + /** + * @param $data + * @return Judge + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\OptimisticLockException + */ + public function create($data): Judge + { + /** @var Judge $judge */ + $judge = $this->hydrator->hydrate($data, new Judge()); + $this->entityManager->persist($judge); + $this->entityManager->flush(); + return $judge; + } + + /** + * @param int $id + * @param $data + * @return Judge + * @throws \Doctrine\ORM\ORMException + * @throws \Doctrine\ORM\OptimisticLockException + */ + public function update(int $id, $data): Judge + { + $judge = $this->entityManager->getRepository(Judge::class)->find($id); + /** @var Judge $judge */ + $judge = $this->hydrator->hydrate($data, $judge); + $this->entityManager->persist($judge); + $this->entityManager->flush(); + return $judge; + } } diff --git a/src/App/Service/JudgeManagerFactory.php b/src/App/Service/JudgeManagerFactory.php index 940acc0..39694cb 100644 --- a/src/App/Service/JudgeManagerFactory.php +++ b/src/App/Service/JudgeManagerFactory.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Service; use Doctrine\ORM\EntityManager; +use DoctrineExpressiveModule\Hydrator\DoctrineObject; use Psr\Container\ContainerInterface; class JudgeManagerFactory @@ -12,6 +13,7 @@ class JudgeManagerFactory public function __invoke(ContainerInterface $container): JudgeManager { $entityManager = $container->get(EntityManager::class); - return new JudgeManager($entityManager); + $hydrator = $container->get(DoctrineObject::class); + return new JudgeManager($entityManager, $hydrator); } } diff --git a/src/App/Service/YearManager.php b/src/App/Service/YearManager.php new file mode 100644 index 0000000..bbefb7f --- /dev/null +++ b/src/App/Service/YearManager.php @@ -0,0 +1,39 @@ +entityManager = $entityManager; + } + + public function getYears() + { + $qb = $this->entityManager->createQueryBuilder(); + $years = $qb->select('jt.year') + ->from(JudgeTitle::class, 'jt') + ->orderBy('jt.year', 'DESC') + ->distinct() + ->getQuery() + ->getArrayResult(); + + $filteredYears = array_map(function($year) { + return $year['year']; + }, $years); + + $thisYear = date("Y"); + if (!in_array($thisYear, $filteredYears)) { + array_unshift($filteredYears, $thisYear); + } + + return $filteredYears; + } +} diff --git a/src/App/Service/YearManagerFactory.php b/src/App/Service/YearManagerFactory.php new file mode 100644 index 0000000..096dee1 --- /dev/null +++ b/src/App/Service/YearManagerFactory.php @@ -0,0 +1,17 @@ +get(EntityManager::class); + return new YearManager($entityManager); + } +} diff --git a/src/DoctrineExpressiveModule/ConfigProvider.php b/src/DoctrineExpressiveModule/ConfigProvider.php new file mode 100644 index 0000000..9a993db --- /dev/null +++ b/src/DoctrineExpressiveModule/ConfigProvider.php @@ -0,0 +1,64 @@ + $this->getDependencies(), + 'form_elements' => $this->getFormElements(), + ]; + } + + /** + * Returns the container dependencies + */ + public function getDependencies() : array + { + return [ + 'aliases' => [ + 'doctrine.hydrator' => Hydrator\DoctrineObject::class, + 'EventManager' => EventManager::class, + ], + 'invokables' => [ + EventManager::class => EventManager::class, + ], + 'factories' => [ + Hydrator\DoctrineObject::class => Hydrator\DoctrineObjectFactory::class, + ], + ]; + } + + /** + * Returns the form dependencies + */ + public function getFormElements() : array + { + return [ + 'aliases' => [ + 'doctrine.object_select' => Form\Element\ObjectSelect::class, + ], + 'factories' => [ + Form\Element\ObjectSelect::class => Form\Element\ElementFactory::class, + ], + ]; + } +} diff --git a/src/DoctrineExpressiveModule/Form/Element/ElementFactory.php b/src/DoctrineExpressiveModule/Form/Element/ElementFactory.php new file mode 100644 index 0000000..0ab0901 --- /dev/null +++ b/src/DoctrineExpressiveModule/Form/Element/ElementFactory.php @@ -0,0 +1,18 @@ +get('doctrine.entity_manager.orm_default'); + /** @var ObjectSelect|ObjectRadio|ObjectMultiCheckbox $element */ + $element = new $elementClass(); + $element->setOption('object_manager', $em); + return $element; + } +} diff --git a/src/DoctrineExpressiveModule/Form/Element/Exception/InvalidRepositoryResultException.php b/src/DoctrineExpressiveModule/Form/Element/Exception/InvalidRepositoryResultException.php new file mode 100644 index 0000000..968e053 --- /dev/null +++ b/src/DoctrineExpressiveModule/Form/Element/Exception/InvalidRepositoryResultException.php @@ -0,0 +1,9 @@ +proxy) { + $this->proxy = new Proxy(); + } + return $this->proxy; + } + + /** + * @param array|\Traversable $options + * @return self + */ + public function setOptions($options) + { + $this->getProxy()->setOptions($options); + return parent::setOptions($options); + } + + /** + * @param string $key + * @param mixed $value + * @return self + */ + public function setOption($key, $value) + { + $this->getProxy()->setOptions([$key => $value]); + return parent::setOption($key, $value); + } + + /** + * {@inheritDoc} + */ + public function setValue($value) + { + if ($value instanceof \Traversable) { + $value = ArrayUtils::iteratorToArray($value); + } elseif ($value == null) { + return parent::setValue([]); + } elseif (! is_array($value)) { + $value = (array)$value; + } + + return parent::setValue(array_map([$this->getProxy(), 'getValue'], $value)); + } + + /** + * {@inheritDoc} + */ + public function getValueOptions() + { + if (! empty($this->valueOptions)) { + return $this->valueOptions; + } + + $proxyValueOptions = $this->getProxy()->getValueOptions(); + + if (! empty($proxyValueOptions)) { + $this->setValueOptions($proxyValueOptions); + } + + return $this->valueOptions; + } +} diff --git a/src/DoctrineExpressiveModule/Form/Element/ObjectRadio.php b/src/DoctrineExpressiveModule/Form/Element/ObjectRadio.php new file mode 100644 index 0000000..05c4315 --- /dev/null +++ b/src/DoctrineExpressiveModule/Form/Element/ObjectRadio.php @@ -0,0 +1,73 @@ +proxy) { + $this->proxy = new Proxy(); + } + return $this->proxy; + } + + /** + * @param array|\Traversable $options + * @return self + */ + public function setOptions($options) + { + $this->getProxy()->setOptions($options); + return parent::setOptions($options); + } + + /** + * @param string $key + * @param mixed $value + * @return self + */ + public function setOption($key, $value) + { + $this->getProxy()->setOptions([$key => $value]); + return parent::setOption($key, $value); + } + + /** + * {@inheritDoc} + */ + public function setValue($value) + { + return parent::setValue($this->getProxy()->getValue($value)); + } + + /** + * {@inheritDoc} + */ + public function getValueOptions() + { + if (! empty($this->valueOptions)) { + return $this->valueOptions; + } + + $proxyValueOptions = $this->getProxy()->getValueOptions(); + + if (! empty($proxyValueOptions)) { + $this->setValueOptions($proxyValueOptions); + } + + return $this->valueOptions; + } +} diff --git a/src/DoctrineExpressiveModule/Form/Element/ObjectSelect.php b/src/DoctrineExpressiveModule/Form/Element/ObjectSelect.php new file mode 100644 index 0000000..2adeacd --- /dev/null +++ b/src/DoctrineExpressiveModule/Form/Element/ObjectSelect.php @@ -0,0 +1,88 @@ +proxy) { + $this->proxy = new Proxy(); + } + return $this->proxy; + } + + /** + * @param array|\Traversable $options + * @return self + */ + public function setOptions($options) + { + $this->getProxy()->setOptions($options); + return parent::setOptions($options); + } + + /** + * @param string $key + * @param mixed $value + * @return self + */ + public function setOption($key, $value) + { + $this->getProxy()->setOptions([$key => $value]); + return parent::setOption($key, $value); + } + + /** + * {@inheritDoc} + */ + public function setValue($value) + { + $multiple = $this->getAttribute('multiple'); + + if (true === $multiple || 'multiple' === $multiple) { + if ($value instanceof \Traversable) { + $value = ArrayUtils::iteratorToArray($value); + } elseif ($value == null) { + return parent::setValue([]); + } elseif (! is_array($value)) { + $value = (array) $value; + } + + return parent::setValue(array_map([$this->getProxy(), 'getValue'], $value)); + } + + return parent::setValue($this->getProxy()->getValue($value)); + } + + /** + * {@inheritDoc} + */ + public function getValueOptions() + { + if (! empty($this->valueOptions)) { + return $this->valueOptions; + } + + $proxyValueOptions = $this->getProxy()->getValueOptions(); + + if (! empty($proxyValueOptions)) { + $this->setValueOptions($proxyValueOptions); + } + + return $this->valueOptions; + } +} diff --git a/src/DoctrineExpressiveModule/Form/Element/Proxy.php b/src/DoctrineExpressiveModule/Form/Element/Proxy.php new file mode 100644 index 0000000..fa73e2a --- /dev/null +++ b/src/DoctrineExpressiveModule/Form/Element/Proxy.php @@ -0,0 +1,653 @@ +setObjectManager($options['object_manager']); + } + + if (isset($options['target_class'])) { + $this->setTargetClass($options['target_class']); + } + + if (isset($options['property'])) { + $this->setProperty($options['property']); + } + + if (isset($options['label_generator'])) { + $this->setLabelGenerator($options['label_generator']); + } + + if (isset($options['find_method'])) { + $this->setFindMethod($options['find_method']); + } + + if (isset($options['is_method'])) { + $this->setIsMethod($options['is_method']); + } + + if (isset($options['display_empty_item'])) { + $this->setDisplayEmptyItem($options['display_empty_item']); + } + + if (isset($options['empty_item_label'])) { + $this->setEmptyItemLabel($options['empty_item_label']); + } + + if (isset($options['option_attributes'])) { + $this->setOptionAttributes($options['option_attributes']); + } + + if (isset($options['optgroup_identifier'])) { + $this->setOptgroupIdentifier($options['optgroup_identifier']); + } + + if (isset($options['optgroup_default'])) { + $this->setOptgroupDefault($options['optgroup_default']); + } + } + + public function getValueOptions() + { + if (empty($this->valueOptions)) { + $this->loadValueOptions(); + } + + return $this->valueOptions; + } + + /** + * @return array|Traversable + */ + public function getObjects() + { + $this->loadObjects(); + + return $this->objects; + } + + /** + * Set the label for the empty option + * + * @param string $emptyItemLabel + * + * @return Proxy + */ + public function setEmptyItemLabel($emptyItemLabel) + { + $this->emptyItemLabel = $emptyItemLabel; + + return $this; + } + + /** + * @return string + */ + public function getEmptyItemLabel() + { + return $this->emptyItemLabel; + } + + /** + * @return array + */ + public function getOptionAttributes() + { + return $this->option_attributes; + } + + /** + * @param array $option_attributes + */ + public function setOptionAttributes(array $option_attributes) + { + $this->option_attributes = $option_attributes; + } + + /** + * Set a flag, whether to include the empty option at the beginning or not + * + * @param boolean $displayEmptyItem + * + * @return Proxy + */ + public function setDisplayEmptyItem($displayEmptyItem) + { + $this->displayEmptyItem = $displayEmptyItem; + + return $this; + } + + /** + * @return boolean + */ + public function getDisplayEmptyItem() + { + return $this->displayEmptyItem; + } + + /** + * Set the object manager + * + * @param ObjectManager $objectManager + * + * @return Proxy + */ + public function setObjectManager(ObjectManager $objectManager) + { + $this->objectManager = $objectManager; + + return $this; + } + + /** + * Get the object manager + * + * @return ObjectManager + */ + public function getObjectManager() + { + return $this->objectManager; + } + + /** + * Set the FQCN of the target object + * + * @param string $targetClass + * + * @return Proxy + */ + public function setTargetClass($targetClass) + { + $this->targetClass = $targetClass; + + return $this; + } + + /** + * Get the target class + * + * @return string + */ + public function getTargetClass() + { + return $this->targetClass; + } + + /** + * Set the property to use as the label in the options + * + * @param string $property + * + * @return Proxy + */ + public function setProperty($property) + { + $this->property = $property; + + return $this; + } + + /** + * @return mixed + */ + public function getProperty() + { + return $this->property; + } + + /** + * Set the label generator callable that is responsible for generating labels for the items in the collection + * + * @param callable $callable A callable used to create a label based off of an Entity + * + * @throws InvalidArgumentException + * + * @return void + */ + public function setLabelGenerator($callable) + { + if (! is_callable($callable)) { + throw new InvalidArgumentException( + 'Property "label_generator" needs to be a callable function or a \Closure' + ); + } + + $this->labelGenerator = $callable; + } + + /** + * @return callable|null + */ + public function getLabelGenerator() + { + return $this->labelGenerator; + } + + /** + * @return string|null + */ + public function getOptgroupIdentifier() + { + return $this->optgroupIdentifier; + } + + /** + * @param string $optgroupIdentifier + */ + public function setOptgroupIdentifier($optgroupIdentifier) + { + $this->optgroupIdentifier = (string) $optgroupIdentifier; + } + + /** + * @return string|null + */ + public function getOptgroupDefault() + { + return $this->optgroupDefault; + } + + /** + * @param string $optgroupDefault + */ + public function setOptgroupDefault($optgroupDefault) + { + $this->optgroupDefault = (string) $optgroupDefault; + } + + /** + * Set if the property is a method to use as the label in the options + * + * @param boolean $method + * + * @return Proxy + */ + public function setIsMethod($method) + { + $this->isMethod = (bool) $method; + + return $this; + } + + /** + * @return mixed + */ + public function getIsMethod() + { + return $this->isMethod; + } + + /** Set the findMethod property to specify the method to use on repository + * + * @param array $findMethod + * + * @return Proxy + */ + public function setFindMethod($findMethod) + { + $this->findMethod = $findMethod; + + return $this; + } + + /** + * Get findMethod definition + * + * @return array + */ + public function getFindMethod() + { + return $this->findMethod; + } + + /** + * @param $targetEntity + * + * @return string|null + */ + protected function generateLabel($targetEntity) + { + if (null === ($labelGenerator = $this->getLabelGenerator())) { + return null; + } + + return call_user_func($labelGenerator, $targetEntity); + } + + /** + * @param $value + * + * @return array|mixed|object + * @throws RuntimeException + */ + public function getValue($value) + { + if (! ($om = $this->getObjectManager())) { + throw new RuntimeException('No object manager was set'); + } + + if (! ($targetClass = $this->getTargetClass())) { + throw new RuntimeException('No target class was set'); + } + + $metadata = $om->getClassMetadata($targetClass); + + if (is_object($value)) { + if ($value instanceof Collection) { + $data = []; + + foreach ($value as $object) { + $values = $metadata->getIdentifierValues($object); + $data[] = array_shift($values); + } + + $value = $data; + } else { + $metadata = $om->getClassMetadata(get_class($value)); + $identifier = $metadata->getIdentifierFieldNames(); + + // TODO: handle composite (multiple) identifiers + if (null !== $identifier && count($identifier) > 1) { + //$value = $key; + } else { + $value = current($metadata->getIdentifierValues($value)); + } + } + } + + return $value; + } + + /** + * Load objects + * + * @throws RuntimeException + * @throws Exception\InvalidRepositoryResultException + * @return void + */ + protected function loadObjects() + { + if (! empty($this->objects)) { + return; + } + + $findMethod = (array) $this->getFindMethod(); + + if (! $findMethod) { + $findMethodName = 'findAll'; + $repository = $this->objectManager->getRepository($this->targetClass); + $objects = $repository->findAll(); + } else { + if (! isset($findMethod['name'])) { + throw new RuntimeException('No method name was set'); + } + $findMethodName = $findMethod['name']; + $findMethodParams = isset($findMethod['params']) ? array_change_key_case($findMethod['params']) : []; + $repository = $this->objectManager->getRepository($this->targetClass); + + if (! method_exists($repository, $findMethodName)) { + throw new RuntimeException( + sprintf( + 'Method "%s" could not be found in repository "%s"', + $findMethodName, + get_class($repository) + ) + ); + } + + $r = new ReflectionMethod($repository, $findMethodName); + $args = []; + + foreach ($r->getParameters() as $param) { + if (array_key_exists(strtolower($param->getName()), $findMethodParams)) { + $args[] = $findMethodParams[strtolower($param->getName())]; + } elseif ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + } elseif (! $param->isOptional()) { + throw new RuntimeException( + sprintf( + 'Required parameter "%s" with no default value for method "%s" in repository "%s"' + . ' was not provided', + $param->getName(), + $findMethodName, + get_class($repository) + ) + ); + } + } + $objects = $r->invokeArgs($repository, $args); + } + + $this->guardForArrayOrTraversable( + $objects, + sprintf('%s::%s() return value', get_class($repository), $findMethodName), + 'DoctrineModule\Form\Element\Exception\InvalidRepositoryResultException' + ); + + $this->objects = $objects; + } + + /** + * Load value options + * + * @throws RuntimeException + * @return void + */ + protected function loadValueOptions() + { + if (! ($om = $this->objectManager)) { + throw new RuntimeException('No object manager was set'); + } + + if (! ($targetClass = $this->targetClass)) { + throw new RuntimeException('No target class was set'); + } + + $metadata = $om->getClassMetadata($targetClass); + $identifier = $metadata->getIdentifierFieldNames(); + $objects = $this->getObjects(); + $options = []; + $optionAttributes = []; + + if ($this->displayEmptyItem) { + $options[''] = $this->getEmptyItemLabel(); + } + + foreach ($objects as $key => $object) { + if (null !== ($generatedLabel = $this->generateLabel($object))) { + $label = $generatedLabel; + } elseif ($property = $this->property) { + if ($this->isMethod == false && ! $metadata->hasField($property)) { + throw new RuntimeException( + sprintf( + 'Property "%s" could not be found in object "%s"', + $property, + $targetClass + ) + ); + } + + $getter = 'get' . Inflector::classify($property); + + if (! is_callable([$object, $getter])) { + throw new RuntimeException( + sprintf('Method "%s::%s" is not callable', $this->targetClass, $getter) + ); + } + + $label = $object->{$getter}(); + } else { + if (! is_callable([$object, '__toString'])) { + throw new RuntimeException( + sprintf( + '%s must have a "__toString()" method defined if you have not set a property' + . ' or method to use.', + $targetClass + ) + ); + } + + $label = (string) $object; + } + + if (null !== $identifier && count($identifier) > 1) { + $value = $key; + } else { + $value = current($metadata->getIdentifierValues($object)); + } + + foreach ($this->getOptionAttributes() as $optionKey => $optionValue) { + if (is_string($optionValue)) { + $optionAttributes[$optionKey] = $optionValue; + + continue; + } + + if (is_callable($optionValue)) { + $callableValue = call_user_func($optionValue, $object); + $optionAttributes[$optionKey] = (string) $callableValue; + + continue; + } + + throw new RuntimeException( + sprintf( + 'Parameter "option_attributes" expects an array of key => value where value is of type' + . '"string" or "callable". Value of type "%s" found.', + gettype($optionValue) + ) + ); + } + + // If no optgroup_identifier has been configured, apply default handling and continue + if (is_null($this->getOptgroupIdentifier())) { + $options[] = ['label' => $label, 'value' => $value, 'attributes' => $optionAttributes]; + + continue; + } + + // optgroup_identifier found, handle grouping + $optgroupGetter = 'get' . Inflector::classify($this->getOptgroupIdentifier()); + + if (! is_callable([$object, $optgroupGetter])) { + throw new RuntimeException( + sprintf('Method "%s::%s" is not callable', $this->targetClass, $optgroupGetter) + ); + } + + $optgroup = $object->{$optgroupGetter}(); + + // optgroup_identifier contains a valid group-name. Handle default grouping. + if (false === is_null($optgroup) && trim($optgroup) !== '') { + $options[$optgroup]['label'] = $optgroup; + $options[$optgroup]['options'][] = [ + 'label' => $label, + 'value' => $value, + 'attributes' => $optionAttributes, + ]; + + continue; + } + + $optgroupDefault = $this->getOptgroupDefault(); + + // No optgroup_default has been provided. Line up without a group + if (is_null($optgroupDefault)) { + $options[] = ['label' => $label, 'value' => $value, 'attributes' => $optionAttributes]; + + continue; + } + + // Line up entry with optgroup_default + $options[$optgroupDefault]['label'] = $optgroupDefault; + $options[$optgroupDefault]['options'][] = [ + 'label' => $label, + 'value' => $value, + 'attributes' => $optionAttributes, + ]; + } + + $this->valueOptions = $options; + } +} diff --git a/src/DoctrineExpressiveModule/Hydrator/DoctrineObject.php b/src/DoctrineExpressiveModule/Hydrator/DoctrineObject.php new file mode 100644 index 0000000..95445e6 --- /dev/null +++ b/src/DoctrineExpressiveModule/Hydrator/DoctrineObject.php @@ -0,0 +1,594 @@ +. + */ + +namespace DoctrineExpressiveModule\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 + */ +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; + } +} diff --git a/src/DoctrineExpressiveModule/Hydrator/DoctrineObjectFactory.php b/src/DoctrineExpressiveModule/Hydrator/DoctrineObjectFactory.php new file mode 100644 index 0000000..d41e6c0 --- /dev/null +++ b/src/DoctrineExpressiveModule/Hydrator/DoctrineObjectFactory.php @@ -0,0 +1,15 @@ +get('doctrine.entity_manager.orm_default'); + return new DoctrineObject($em); + } +} diff --git a/src/DoctrineExpressiveModule/Hydrator/Filter/PropertyName.php b/src/DoctrineExpressiveModule/Hydrator/Filter/PropertyName.php new file mode 100644 index 0000000..c1c9067 --- /dev/null +++ b/src/DoctrineExpressiveModule/Hydrator/Filter/PropertyName.php @@ -0,0 +1,66 @@ +. + */ + +namespace DoctrineExpressiveModule\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 + */ +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; + } +} diff --git a/src/DoctrineExpressiveModule/Hydrator/Strategy/AbstractCollectionStrategy.php b/src/DoctrineExpressiveModule/Hydrator/Strategy/AbstractCollectionStrategy.php new file mode 100644 index 0000000..2eff700 --- /dev/null +++ b/src/DoctrineExpressiveModule/Hydrator/Strategy/AbstractCollectionStrategy.php @@ -0,0 +1,190 @@ +. + */ + +namespace DoctrineExpressiveModule\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 + */ +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)); + } +} diff --git a/src/DoctrineExpressiveModule/Hydrator/Strategy/AllowRemoveByReference.php b/src/DoctrineExpressiveModule/Hydrator/Strategy/AllowRemoveByReference.php new file mode 100644 index 0000000..94c33ef --- /dev/null +++ b/src/DoctrineExpressiveModule/Hydrator/Strategy/AllowRemoveByReference.php @@ -0,0 +1,58 @@ +. + */ + +namespace DoctrineExpressiveModule\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 + */ +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; + } +} diff --git a/src/DoctrineExpressiveModule/Hydrator/Strategy/AllowRemoveByValue.php b/src/DoctrineExpressiveModule/Hydrator/Strategy/AllowRemoveByValue.php new file mode 100644 index 0000000..30a2806 --- /dev/null +++ b/src/DoctrineExpressiveModule/Hydrator/Strategy/AllowRemoveByValue.php @@ -0,0 +1,76 @@ +. + */ + +namespace DoctrineExpressiveModule\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 + */ +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; + } +} diff --git a/src/DoctrineExpressiveModule/Hydrator/Strategy/DisallowRemoveByReference.php b/src/DoctrineExpressiveModule/Hydrator/Strategy/DisallowRemoveByReference.php new file mode 100644 index 0000000..47eadc2 --- /dev/null +++ b/src/DoctrineExpressiveModule/Hydrator/Strategy/DisallowRemoveByReference.php @@ -0,0 +1,53 @@ +. + */ + +namespace DoctrineExpressiveModule\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 + */ +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; + } +} diff --git a/src/DoctrineExpressiveModule/Hydrator/Strategy/DisallowRemoveByValue.php b/src/DoctrineExpressiveModule/Hydrator/Strategy/DisallowRemoveByValue.php new file mode 100644 index 0000000..77df2be --- /dev/null +++ b/src/DoctrineExpressiveModule/Hydrator/Strategy/DisallowRemoveByValue.php @@ -0,0 +1,72 @@ +. + */ + +namespace DoctrineExpressiveModule\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 + */ +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; + } +} diff --git a/src/DoctrineExpressiveModule/Middleware/CorsMiddlewareFactory.php b/src/DoctrineExpressiveModule/Middleware/CorsMiddlewareFactory.php new file mode 100644 index 0000000..2d6143e --- /dev/null +++ b/src/DoctrineExpressiveModule/Middleware/CorsMiddlewareFactory.php @@ -0,0 +1,18 @@ + ["Authorization", "If-Match", "If-Unmodified-Since", "Content-type"], + ]); + } +} \ No newline at end of file diff --git a/src/DoctrineExpressiveModule/Validator/NoObjectExists.php b/src/DoctrineExpressiveModule/Validator/NoObjectExists.php new file mode 100644 index 0000000..a28b88a --- /dev/null +++ b/src/DoctrineExpressiveModule/Validator/NoObjectExists.php @@ -0,0 +1,43 @@ + + */ +class NoObjectExists extends ObjectExists +{ + /** + * Error constants + */ + const ERROR_OBJECT_FOUND = 'objectFound'; + + /** + * @var array Message templates + */ + protected $messageTemplates = [ + self::ERROR_OBJECT_FOUND => "An object matching '%value%' was found", + ]; + + /** + * {@inheritDoc} + */ + public function isValid($value) + { + $cleanedValue = $this->cleanSearchValue($value); + $match = $this->objectRepository->findOneBy($cleanedValue); + + if (is_object($match)) { + $this->error(self::ERROR_OBJECT_FOUND, $value); + + return false; + } + + return true; + } +} diff --git a/src/DoctrineExpressiveModule/Validator/ObjectExists.php b/src/DoctrineExpressiveModule/Validator/ObjectExists.php new file mode 100644 index 0000000..3b67ad0 --- /dev/null +++ b/src/DoctrineExpressiveModule/Validator/ObjectExists.php @@ -0,0 +1,175 @@ + + */ +class ObjectExists extends AbstractValidator +{ + /** + * Error constants + */ + const ERROR_NO_OBJECT_FOUND = 'noObjectFound'; + + /** + * @var array Message templates + */ + protected $messageTemplates = [ + self::ERROR_NO_OBJECT_FOUND => "No object matching '%value%' was found", + ]; + + /** + * ObjectRepository from which to search for entities + * + * @var ObjectRepository + */ + protected $objectRepository; + + /** + * Fields to be checked + * + * @var array + */ + protected $fields; + + /** + * Constructor + * + * @param array $options required keys are `object_repository`, which must be an instance of + * Doctrine\Common\Persistence\ObjectRepository, and `fields`, with either + * a string or an array of strings representing the fields to be matched by the validator. + * @throws \Zend\Validator\Exception\InvalidArgumentException + */ + public function __construct(array $options) + { + if (! isset($options['object_repository']) || ! $options['object_repository'] instanceof ObjectRepository) { + if (! array_key_exists('object_repository', $options)) { + $provided = 'nothing'; + } else { + if (is_object($options['object_repository'])) { + $provided = get_class($options['object_repository']); + } else { + $provided = getType($options['object_repository']); + } + } + + throw new Exception\InvalidArgumentException( + sprintf( + 'Option "object_repository" is required and must be an instance of' + . ' Doctrine\Common\Persistence\ObjectRepository, %s given', + $provided + ) + ); + } + + $this->objectRepository = $options['object_repository']; + + if (! isset($options['fields'])) { + throw new Exception\InvalidArgumentException( + 'Key `fields` must be provided and be a field or a list of fields to be used when searching for' + . ' existing instances' + ); + } + + $this->fields = $options['fields']; + $this->validateFields(); + + parent::__construct($options); + } + + /** + * Filters and validates the fields passed to the constructor + * + * @throws \Zend\Validator\Exception\InvalidArgumentException + * @return array + */ + private function validateFields() + { + $fields = (array) $this->fields; + + if (empty($fields)) { + throw new Exception\InvalidArgumentException('Provided fields list was empty!'); + } + + foreach ($fields as $key => $field) { + if (! is_string($field)) { + throw new Exception\InvalidArgumentException( + sprintf('Provided fields must be strings, %s provided for key %s', gettype($field), $key) + ); + } + } + + $this->fields = array_values($fields); + } + + /** + * @param string|array $value a field value or an array of field values if more fields have been configured to be + * matched + * @return array + * @throws \Zend\Validator\Exception\RuntimeException + */ + protected function cleanSearchValue($value) + { + $value = is_object($value) ? [$value] : (array) $value; + + if (ArrayUtils::isHashTable($value)) { + $matchedFieldsValues = []; + + foreach ($this->fields as $field) { + if (! array_key_exists($field, $value)) { + throw new Exception\RuntimeException( + sprintf( + 'Field "%s" was not provided, but was expected since the configured field lists needs' + . ' it for validation', + $field + ) + ); + } + + $matchedFieldsValues[$field] = $value[$field]; + } + } else { + $matchedFieldsValues = @array_combine($this->fields, $value); + + if (false === $matchedFieldsValues) { + throw new Exception\RuntimeException( + sprintf( + 'Provided values count is %s, while expected number of fields to be matched is %s', + count($value), + count($this->fields) + ) + ); + } + } + + return $matchedFieldsValues; + } + + /** + * {@inheritDoc} + */ + public function isValid($value) + { + $cleanedValue = $this->cleanSearchValue($value); + $match = $this->objectRepository->findOneBy($cleanedValue); + + if (is_object($match)) { + return true; + } + + $this->error(self::ERROR_NO_OBJECT_FOUND, $value); + + return false; + } +} diff --git a/src/DoctrineExpressiveModule/Validator/Service/AbstractValidatorFactory.php b/src/DoctrineExpressiveModule/Validator/Service/AbstractValidatorFactory.php new file mode 100644 index 0000000..d0a2bac --- /dev/null +++ b/src/DoctrineExpressiveModule/Validator/Service/AbstractValidatorFactory.php @@ -0,0 +1,121 @@ + + */ +abstract class AbstractValidatorFactory implements FactoryInterface +{ + const DEFAULT_OBJECTMANAGER_KEY = 'doctrine.entitymanager.orm_default'; + + protected $creationOptions = []; + + protected $validatorClass; + + /** + * @param ContainerInterface $container + * @param array $options + * @return \Doctrine\Common\Persistence\ObjectRepository + * @throws ServiceCreationException + */ + protected function getRepository(ContainerInterface $container, array $options) + { + if (empty($options['target_class'])) { + throw new ServiceCreationException(sprintf( + "Option 'target_class' is missing when creating validator %s", + __CLASS__ + )); + } + + $objectManager = $this->getObjectManager($container, $options); + $targetClassName = $options['target_class']; + $objectRepository = $objectManager->getRepository($targetClassName); + + return $objectRepository; + } + + /** + * @param ContainerInterface $container + * @param array $options + * @return \Doctrine\Common\Persistence\ObjectManager + */ + protected function getObjectManager(ContainerInterface $container, array $options) + { + $objectManager = ($options['object_manager']) ?? self::DEFAULT_OBJECTMANAGER_KEY; + + if (is_string($objectManager)) { + $objectManager = $container->get($objectManager); + } + + return $objectManager; + } + + /** + * @param array $options + * @return array + */ + protected function getFields(array $options) + { + if (isset($options['fields'])) { + return (array) $options['fields']; + } + + return []; + } + + /** + * Helper to merge options array passed to `__invoke` + * together with the options array created based on the above + * helper methods. + * + * @param array $previousOptions + * @param array $newOptions + * @return array + */ + protected function merge($previousOptions, $newOptions) + { + return ArrayUtils::merge($previousOptions, $newOptions, true); + } + + /** + * Helper method for ZF2 compatiblity. + * + * In ZF2 the plugin manager instance if passed to `createService` + * instead of the global service manager instance (as in ZF3). + * + * @param ContainerInterface $container + * @return ContainerInterface + */ + protected function container(ContainerInterface $container) + { + if ($container instanceof ServiceLocatorAwareInterface) { + $container = $container->getServiceLocator(); + } + + return $container; + } + + public function createService(ServiceLocatorInterface $serviceLocator) + { + return $this($serviceLocator, $this->validatorClass, $this->creationOptions); + } + + public function setCreationOptions(array $options) + { + $this->creationOptions = $options; + } +} diff --git a/src/DoctrineExpressiveModule/Validator/Service/Exception/ServiceCreationException.php b/src/DoctrineExpressiveModule/Validator/Service/Exception/ServiceCreationException.php new file mode 100644 index 0000000..62d76d6 --- /dev/null +++ b/src/DoctrineExpressiveModule/Validator/Service/Exception/ServiceCreationException.php @@ -0,0 +1,16 @@ + + */ +class ServiceCreationException extends BaseRuntimeException +{ +} diff --git a/src/DoctrineExpressiveModule/Validator/Service/NoObjectExistsFactory.php b/src/DoctrineExpressiveModule/Validator/Service/NoObjectExistsFactory.php new file mode 100644 index 0000000..84b9cae --- /dev/null +++ b/src/DoctrineExpressiveModule/Validator/Service/NoObjectExistsFactory.php @@ -0,0 +1,34 @@ + + */ +class NoObjectExistsFactory extends AbstractValidatorFactory +{ + protected $validatorClass = NoObjectExists::class; + + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + $container = $this->container($container); + + $repository = $this->getRepository($container, $options); + + $validator = new NoObjectExists($this->merge($options, [ + 'object_repository' => $repository, + 'fields' => $this->getFields($options), + ])); + + return $validator; + } +} diff --git a/src/DoctrineExpressiveModule/Validator/Service/ObjectExistsFactory.php b/src/DoctrineExpressiveModule/Validator/Service/ObjectExistsFactory.php new file mode 100644 index 0000000..53d7ac2 --- /dev/null +++ b/src/DoctrineExpressiveModule/Validator/Service/ObjectExistsFactory.php @@ -0,0 +1,34 @@ + + */ +class ObjectExistsFactory extends AbstractValidatorFactory +{ + protected $validatorClass = ObjectExists::class; + + public function __invoke(ContainerInterface $container, $requestedName, array $options = null) + { + $container = $this->container($container); + + $repository = $this->getRepository($container, $options); + + $validator = new ObjectExists($this->merge($options, [ + 'object_repository' => $repository, + 'fields' => $this->getFields($options), + ])); + + return $validator; + } +} diff --git a/src/DoctrineExpressiveModule/Validator/Service/UniqueObjectFactory.php b/src/DoctrineExpressiveModule/Validator/Service/UniqueObjectFactory.php new file mode 100644 index 0000000..c792c27 --- /dev/null +++ b/src/DoctrineExpressiveModule/Validator/Service/UniqueObjectFactory.php @@ -0,0 +1,28 @@ +container($container); + + $useContext = isset($options['use_context']) ? (boolean) $options['use_context'] : false; + + $validator = new UniqueObject($this->merge($options, [ + 'object_manager' => $this->getObjectManager($container, $options), + 'use_context' => $useContext, + 'object_repository' => $this->getRepository($container, $options), + 'fields' => $this->getFields($options), + ])); + + return $validator; + } +} diff --git a/src/DoctrineExpressiveModule/Validator/UniqueObject.php b/src/DoctrineExpressiveModule/Validator/UniqueObject.php new file mode 100644 index 0000000..a86e939 --- /dev/null +++ b/src/DoctrineExpressiveModule/Validator/UniqueObject.php @@ -0,0 +1,166 @@ + + */ +class UniqueObject extends ObjectExists +{ + /** + * Error constants + */ + const ERROR_OBJECT_NOT_UNIQUE = 'objectNotUnique'; + + /** + * @var array Message templates + */ + protected $messageTemplates = [ + self::ERROR_OBJECT_NOT_UNIQUE => "There is already another object matching '%value%'", + ]; + + /** + * @var ObjectManager + */ + protected $objectManager; + + /** + * @var boolean + */ + protected $useContext; + + /*** + * Constructor + * + * @param array $options required keys are `object_repository`, which must be an instance of + * Doctrine\Common\Persistence\ObjectRepository, `object_manager`, which + * must be an instance of Doctrine\Common\Persistence\ObjectManager, + * and `fields`, with either a string or an array of strings representing + * the fields to be matched by the validator. + * @throws Exception\InvalidArgumentException + */ + public function __construct(array $options) + { + parent::__construct($options); + + if (! isset($options['object_manager']) || ! $options['object_manager'] instanceof ObjectManager) { + if (! array_key_exists('object_manager', $options)) { + $provided = 'nothing'; + } else { + if (is_object($options['object_manager'])) { + $provided = get_class($options['object_manager']); + } else { + $provided = getType($options['object_manager']); + } + } + + throw new Exception\InvalidArgumentException( + sprintf( + 'Option "object_manager" is required and must be an instance of' + . ' Doctrine\Common\Persistence\ObjectManager, %s given', + $provided + ) + ); + } + + $this->objectManager = $options['object_manager']; + $this->useContext = isset($options['use_context']) ? (boolean) $options['use_context'] : false; + } + + /** + * Returns false if there is another object with the same field values but other identifiers. + * + * @param mixed $value + * @param array $context + * @return boolean + */ + public function isValid($value, $context = null) + { + if (! $this->useContext) { + $context = (array) $value; + } + + $cleanedValue = $this->cleanSearchValue($value); + $match = $this->objectRepository->findOneBy($cleanedValue); + + if (! is_object($match)) { + return true; + } + + $expectedIdentifiers = $this->getExpectedIdentifiers($context); + $foundIdentifiers = $this->getFoundIdentifiers($match); + + if (count(array_diff_assoc($expectedIdentifiers, $foundIdentifiers)) == 0) { + return true; + } + + $this->error(self::ERROR_OBJECT_NOT_UNIQUE, $value); + return false; + } + + /** + * Gets the identifiers from the matched object. + * + * @param object $match + * @return array + * @throws Exception\RuntimeException + */ + protected function getFoundIdentifiers($match) + { + return $this->objectManager + ->getClassMetadata($this->objectRepository->getClassName()) + ->getIdentifierValues($match); + } + + /** + * Gets the identifiers from the context. + * + * @param array|object $context + * @return array + * @throws Exception\RuntimeException + */ + protected function getExpectedIdentifiers($context = null) + { + if ($context === null) { + throw new Exception\RuntimeException( + 'Expected context to be an array but is null' + ); + } + + $className = $this->objectRepository->getClassName(); + + if ($context instanceof $className) { + return $this->objectManager + ->getClassMetadata($this->objectRepository->getClassName()) + ->getIdentifierValues($context); + } + + $result = []; + foreach ($this->getIdentifiers() as $identifierField) { + if (! array_key_exists($identifierField, $context)) { + throw new Exception\RuntimeException(\sprintf('Expected context to contain %s', $identifierField)); + } + + $result[$identifierField] = $context[$identifierField]; + } + return $result; + } + + + /** + * @return array the names of the identifiers + */ + protected function getIdentifiers() + { + return $this->objectManager + ->getClassMetadata($this->objectRepository->getClassName()) + ->getIdentifierFieldNames(); + } +}