diff --git a/.gitignore b/.gitignore index 9f599ab..b2bab24 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,5 @@ c3.php # local app configs config/params-local.php + +/docker/postgres/data/* diff --git a/composer.json b/composer.json index 5327bad..07e6009 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "yiisoft/log-target-file": "@dev", "yiisoft/mailer": "@dev", "yiisoft/mailer-swiftmailer": "@dev", + "yiisoft/request-body-parser": "^3.0@dev", "yiisoft/router": "@dev", "yiisoft/router-fastroute": "@dev", "yiisoft/view": "@dev", diff --git a/composer.lock b/composer.lock index 930c50c..07a0b54 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "62492dfd49a826fbe86c758097aa00e0", + "content-hash": "1468f07f5e47f5bb6e0dc10bb57b0eb4", "packages": [ { "name": "alexkart/curl-builder", @@ -5565,6 +5565,72 @@ ], "time": "2020-08-09T18:33:44+00:00" }, + { + "name": "yiisoft/request-body-parser", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/yiisoft/request-body-parser.git", + "reference": "2f54166e57b8972217e4b93c444f0622ea0550b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/yiisoft/request-body-parser/zipball/2f54166e57b8972217e4b93c444f0622ea0550b7", + "reference": "2f54166e57b8972217e4b93c444f0622ea0550b7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.4|^8.0", + "psr/container": "1.0.0", + "psr/http-message": "^1.0", + "psr/http-message-implementation": "1.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "yiisoft/http": "1.0.x-dev" + }, + "require-dev": { + "infection/infection": "^0.16.3", + "nyholm/psr7": "^1.0", + "phan/phan": "^3.0", + "phpunit/phpunit": "^9.3", + "yiisoft/di": "^3.0@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Yiisoft\\Request\\Body\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Middleware for parsing various data formats", + "homepage": "https://www.yiiframework.com/", + "keywords": [ + "body", + "middleware", + "parser", + "yii3" + ], + "funding": [ + { + "url": "https://github.com/yiisoft", + "type": "github" + }, + { + "url": "https://opencollective.com/yiisoft", + "type": "open_collective" + } + ], + "time": "2020-08-09T18:32:25+00:00" + }, { "name": "yiisoft/router", "version": "dev-master", @@ -9346,6 +9412,7 @@ "yiisoft/log-target-file": 20, "yiisoft/mailer": 20, "yiisoft/mailer-swiftmailer": 20, + "yiisoft/request-body-parser": 20, "yiisoft/router": 20, "yiisoft/router-fastroute": 20, "yiisoft/view": 20, @@ -9363,5 +9430,6 @@ "platform": { "php": "^7.4" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "1.1.0" } diff --git a/config/common.php b/config/common.php index f62fc61..a2ffdc4 100644 --- a/config/common.php +++ b/config/common.php @@ -2,10 +2,20 @@ declare(strict_types=1); -use App\Service\Mailer; +use App\Common\Domain\Entity\Identity; +use App\Common\Service\Mailer; +use App\Module\Contact\Api\ContactMailer; +use App\Module\Contact\Service\MailerService; +use App\Module\Link\Api\UserLinkService; +use App\Module\Link\Domain\Entity\Link; +use App\Module\Link\Domain\Repository\LinkRepository; +use App\Module\Link\Service\UserLink; +use Cycle\ORM\ORMInterface; use Psr\Container\ContainerInterface; use Yiisoft\Aliases\Aliases; +use Yiisoft\Auth\AuthInterface; use Yiisoft\Auth\IdentityRepositoryInterface; +use Yiisoft\Auth\Method\HttpHeader; use Yiisoft\Factory\Definitions\Reference; /* @var array $params */ @@ -14,21 +24,29 @@ ContainerInterface::class => static function (ContainerInterface $container) { return $container; }, - Aliases::class => [ '__class' => Aliases::class, '__construct()' => [$params['aliases']], ], - - \App\Module\Contact\Api\ContactMailer::class => static function (ContainerInterface $container) use ($params) { - return (new \App\Module\Contact\Service\MailerService( + ContactMailer::class => static function (ContainerInterface $container) use ($params) { + return (new MailerService( $container->get(Mailer::class), $params['mailer']['adminEmail'] )); }, - IdentityRepositoryInterface::class => static function (ContainerInterface $container) { - return $container->get(\Cycle\ORM\ORMInterface::class) - ->getRepository(\App\Common\Domain\Entity\Identity::class); + return $container->get(ORMInterface::class) + ->getRepository(Identity::class); + }, + LinkRepository::class => static function (ContainerInterface $container) { + return $container->get(ORMInterface::class) + ->getRepository(Link::class); + }, + AuthInterface::class => static function (ContainerInterface $container) { + $httHeader = $container->get(HttpHeader::class); + $httHeader->setHeaderName('Authorization'); + + return $httHeader; }, + UserLinkService::class => Reference::to(UserLink::class), ]; diff --git a/config/params.php b/config/params.php index 0cd5027..246eee2 100644 --- a/config/params.php +++ b/config/params.php @@ -3,7 +3,6 @@ declare(strict_types=1); use App\Common\Application\ApplicationParameters; -use Cycle\Schema\Generator; use Psr\Log\LogLevel; use Yiisoft\Assets\AssetManager; use Yiisoft\Form\Widget\Field; @@ -143,25 +142,19 @@ * and its config as value: */ 'schema-providers' => [ - // \Yiisoft\Yii\Cycle\Schema\Provider\SimpleCacheSchemaProvider::class => [ - // 'key' => 'db-schema' - // ], - // \Yiisoft\Yii\Cycle\Schema\Provider\FromFileSchemaProvider::class => [ - // 'file' => '@runtime/cycle-schema.php' - // ], // \Yiisoft\Yii\Cycle\Schema\Provider\FromConveyorSchemaProvider::class => [ // 'generators' => [ - // // Generator\SyncTables::class, // sync table changes to database + // // \Cycle\Schema\Generator\SyncTables::class, // sync table changes to database // ] // ], ], - /** * {@see \Yiisoft\Yii\Cycle\Schema\Conveyor\AnnotatedSchemaConveyor} settings * A list of entity directories. You can use {@see \Yiisoft\Aliases\Aliases} in paths. */ 'annotated-entity-paths' => [ '@src/Common/Domain/Entity', + '@src/Module/Link/Domain/Entity', ], ], diff --git a/config/providers.php b/config/providers.php index 8520a59..c00708d 100644 --- a/config/providers.php +++ b/config/providers.php @@ -5,7 +5,6 @@ /* @var array $params */ use App\Common\Application\Provider\CacheProvider; -use App\Common\Application\Provider\EventDispatcherProvider; use App\Common\Application\Provider\FileRotatorProvider; use App\Common\Application\Provider\FileTargetProvider; use App\Common\Application\Provider\LoggerProvider; diff --git a/config/routes.php b/config/routes.php index c340860..24f1f1f 100644 --- a/config/routes.php +++ b/config/routes.php @@ -2,21 +2,44 @@ declare(strict_types=1); +use App\Api\External\Controller\LinkController; +use App\Api\External\Controller\UserController; use App\Api\UI\Controller\ContactController; use App\Api\UI\Controller\SiteController; +use Yiisoft\Auth\Middleware\Auth; use Yiisoft\Http\Method; use Yiisoft\Router\Group; use Yiisoft\Router\Route; +use roxblnfk\SmartStream\Middleware\BucketStreamMiddleware; +use Yiisoft\Request\Body\RequestBodyParser; return [ + // UI Route::get('/', [SiteController::class, 'index'])->name('site/index'), Route::get('/about', [SiteController::class, 'about'])->name('site/about'), Route::methods([Method::GET, Method::POST], '/contact', [ContactController::class, 'contact']) ->name('contact/form'), + // External API - Group::create('/api', [ - Route::anyMethod('/link', \App\Api\External\Controller\LinkController::class)->name('api/link'), - Route::anyMethod('/user', \App\Api\External\Controller\UserController::class)->name('api/user'), + Group::create( + '/api', + [ + Route::get('/user', UserController::class)->name('api/user/get')->addMiddleware(Auth::class), + Route::post('/user', UserController::class)->name('api/user/post'), + Route::delete('/user', UserController::class)->name('api/user/delete'), + Route::put('/user', UserController::class)->name('api/user/put'), + Route::patch('/user', UserController::class)->name('api/user/patch'), + Route::options('/user', UserController::class)->name('api/user/options'), + Route::head('/user', UserController::class)->name('api/user/head'), - ])->addMiddleware(\roxblnfk\SmartStream\Middleware\BucketStreamMiddleware::class), + Route::get('/link', LinkController::class)->name('api/link/get')->addMiddleware(Auth::class), + Route::post('/link', LinkController::class)->name('api/link/post')->addMiddleware(Auth::class), + Route::delete('/link', LinkController::class)->name('api/link/delete')->addMiddleware(Auth::class), + Route::put('/link', LinkController::class)->name('api/link/put'), + Route::patch('/link', LinkController::class)->name('api/link/patch'), + Route::options('/link', LinkController::class)->name('api/link/options'), + Route::head('/link', LinkController::class)->name('api/link/head'), + ] + )->addMiddleware(BucketStreamMiddleware::class) + ->addMiddleware(RequestBodyParser::class) ]; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a13925b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3' + +services: + php: + container_name: digest-php + image: yiisoftware/yii-php:7.4-apache + working_dir: /app + ports: + - 8080:80 + volumes: + - ./:/app + - ~/.composer-docker/cache:/root/.composer/cache:delegated + + postgresql: + container_name: postgresql + image: postgres:10.12 + env_file: + - ./docker/postgres/database.env + ports: + - 5432:5432 + volumes: + - ./docker/postgres/data/:/var/lib/postgresql/data/ + restart: always + diff --git a/docker/postgres/database.env b/docker/postgres/database.env new file mode 100644 index 0000000..2e19055 --- /dev/null +++ b/docker/postgres/database.env @@ -0,0 +1,3 @@ +POSTGRES_USER=postgres +POSTGRES_PASSWORD=pass +POSTGRES_DB=default diff --git a/resources/migrations/20200815.160455_0_default_create_link.php b/resources/migrations/20200815.160455_0_default_create_link.php new file mode 100644 index 0000000..d486ee8 --- /dev/null +++ b/resources/migrations/20200815.160455_0_default_create_link.php @@ -0,0 +1,57 @@ +table('link_suggestion') + ->addColumn('id', 'primary', [ + 'nullable' => false, + 'default' => null + ]) + ->addColumn('url', 'string', [ + 'nullable' => false, + 'default' => null, + 'size' => 255 + ]) + ->addColumn('description', 'string', [ + 'nullable' => true, + 'default' => null, + 'size' => 255 + ]) + ->addColumn('created_at', 'datetime', [ + 'nullable' => false, + 'default' => null + ]) + ->addColumn('updated_at', 'datetime', [ + 'nullable' => false, + 'default' => null + ]) + ->addColumn('identity_id', 'integer', [ + 'nullable' => false, + 'default' => null + ]) + ->addIndex(["identity_id"], [ + 'name' => 'link_suggestion_index_identity_id_5f3911c14898c', + 'unique' => false + ]) + ->addForeignKey(["identity_id"], 'identity', ["id"], [ + 'name' => 'link_suggestion_foreign_identity_id_5f3911c14899c', + 'delete' => 'CASCADE', + 'update' => 'CASCADE' + ]) + ->setPrimaryKeys(["id"]) + ->create(); + } + + public function down() + { + $this->table('link_suggestion')->drop(); + } +} diff --git a/resources/views/contact/form.php b/resources/views/contact/form.php index 7e56960..5ee0669 100644 --- a/resources/views/contact/form.php +++ b/resources/views/contact/form.php @@ -5,7 +5,7 @@ use Yiisoft\Form\Widget\Form; use Yiisoft\Html\Html; -/* @var App\Form\ContactForm $form */ +/* @var \App\Api\UI\Form\ContactForm $form */ /* @var Yiisoft\Router\UrlGeneratorInterface $url */ /* @var string|null $csrf */ /* @var Yiisoft\Form\Widget\Field $field */ diff --git a/src/Api/External/Controller/ApiController.php b/src/Api/External/Controller/ApiController.php index 4786dc6..1ad26d8 100644 --- a/src/Api/External/Controller/ApiController.php +++ b/src/Api/External/Controller/ApiController.php @@ -4,35 +4,37 @@ namespace App\Api\External\Controller; +use App\Api\External\Data\ApiBucket; use App\Api\External\Data\ErrorBucket; use App\Api\External\Exception\HttpException; +use App\Common\Domain\Entity\Identity; +use App\Common\Domain\Exception\EntityNotFound; +use ErrorException; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use roxblnfk\SmartStream\Data\DataBucket; -use roxblnfk\SmartStream\SmartStreamFactory; use Throwable; +use Yiisoft\Auth\Middleware\Auth; use Yiisoft\Http\Header; use Yiisoft\Http\Method; use Yiisoft\Http\Status; use Yiisoft\Injector\Injector; +use roxblnfk\SmartStream\Data\DataBucket; +use roxblnfk\SmartStream\SmartStreamFactory; abstract class ApiController implements MiddlewareInterface { protected ResponseFactoryInterface $responseFactory; - protected SmartStreamFactory $smartStreamFactory; private Injector $injector; public function __construct( ResponseFactoryInterface $responseFactory, - SmartStreamFactory $smartStreamFactory, Injector $injector ) { $this->responseFactory = $responseFactory; - $this->smartStreamFactory = $smartStreamFactory; $this->injector = $injector; } @@ -52,8 +54,16 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $this->prepareResponse($data, $request); } - protected function errorToBucket(Throwable $error): DataBucket { - $bucket = new ErrorBucket($error, false); + protected function errorToBucket(Throwable $error): DataBucket + { + $bucket = new ErrorBucket($error); + + if ($error instanceof EntityNotFound) { + $bucket = $bucket->withStatusCode(Status::NOT_FOUND); + } elseif ($error instanceof ErrorException) { + $bucket = $bucket->withStatusCode(Status::INTERNAL_SERVER_ERROR); + } + return $bucket; } @@ -65,7 +75,11 @@ protected function prepareResponse($data, ?ServerRequestInterface $request = nul if ($data instanceof StreamInterface) { $stream = $data; } else { - $stream = $this->smartStreamFactory->createStream($data, $request); + $smartStreamFactory = $this->injector->make( + SmartStreamFactory::class, + ['defaultBucketClass' => ApiBucket::class] + ); + $stream = $smartStreamFactory->createStream($data, $request); } return $this->responseFactory->createResponse()->withBody($stream); } @@ -80,4 +94,14 @@ protected function getAllowedMethods(): array } return $result; } + + protected function getIdentityFromRequest(ServerRequestInterface $request): Identity + { + /** + * @var Identity $identity + */ + $identity = $request->getAttribute(Auth::REQUEST_NAME); + + return $identity; + } } diff --git a/src/Api/External/Controller/LinkController.php b/src/Api/External/Controller/LinkController.php index f729ff3..d122b8d 100644 --- a/src/Api/External/Controller/LinkController.php +++ b/src/Api/External/Controller/LinkController.php @@ -4,32 +4,41 @@ namespace App\Api\External\Controller; +use App\Api\External\Exception\HttpException; +use App\Module\Link\Api\UserLinkService; +use App\Module\Link\Domain\Validation\CreateLinkForm; +use App\Module\Link\Domain\Validation\FindLinkForm; use Psr\Http\Message\ServerRequestInterface; +use Yiisoft\Form\FormModel; +use Yiisoft\Http\Status; -class LinkController extends ApiController +final class LinkController extends ApiController { - public function get(ServerRequestInterface $request) + public function get(UserLinkService $service, ServerRequestInterface $request, FindLinkForm $form): array { - $url = $request->getQueryParams()['url'] ?? null; - $this->validateLink($url); - return 'Link is valid.'; + $this->validateLinkForm($form, $request->getQueryParams()); + $link = $service->getLink($form->getUrl(), $this->getIdentityFromRequest($request)); + + return ['url' => $link->getUrl()]; } - public function post() + + public function post(UserLinkService $service, ServerRequestInterface $request, CreateLinkForm $form): void { - return __METHOD__; + $this->validateLinkForm($form, $request->getParsedBody()); + $service->createLink($form, $this->getIdentityFromRequest($request)); } - public function delete() + + public function delete(UserLinkService $service, ServerRequestInterface $request, FindLinkForm $form): void { - return __METHOD__; + $this->validateLinkForm($form, $request->getQueryParams()); + $service->deleteLink($form->getUrl(), $this->getIdentityFromRequest($request)); } - protected function validateLink($link): void + private function validateLinkForm(FormModel $form, array $data): void { - if (!is_string($link)) { - throw new \InvalidArgumentException('Link should be string.'); - } - if (!filter_var($link, FILTER_VALIDATE_URL)) { - throw new \InvalidArgumentException('Invalid link.'); + $form->setAttributes($data); + if (!$form->validate()) { + throw new HttpException(Status::BAD_REQUEST, current($form->firstErrors())); } } } diff --git a/src/Api/External/Controller/UserController.php b/src/Api/External/Controller/UserController.php index 950379c..b797c47 100644 --- a/src/Api/External/Controller/UserController.php +++ b/src/Api/External/Controller/UserController.php @@ -4,39 +4,26 @@ namespace App\Api\External\Controller; -use App\Api\External\Data\ApiBucket; +use App\Api\External\Exception\HttpException; use App\Common\Domain\Service\IdentityService; use Psr\Http\Message\ServerRequestInterface; -use roxblnfk\SmartStream\Data\DataBucket; -use Yiisoft\Auth\IdentityRepositoryInterface; +use Yiisoft\Http\Status; class UserController extends ApiController { - public function get(ServerRequestInterface $request, IdentityRepositoryInterface $repository): DataBucket + public function get(ServerRequestInterface $request): array { - $token = $request->getQueryParams()['token'] ?? null; - $this->validateToken($token); - $identity = $repository->findIdentityByToken($token, ''); + $identity = $this->getIdentityFromRequest($request); if ($identity === null) { - throw new \RuntimeException('User not found.'); + throw new HttpException(Status::NOT_FOUND, 'User not found.'); } - return new ApiBucket(['id' => $identity->getId()]); + return ['id' => $identity->getId()]; } - public function post(ServerRequestInterface $request, IdentityService $service): DataBucket + public function post(ServerRequestInterface $request, IdentityService $service): array { - $data = $request->getParsedBody(); - $identity = $service->createIdentity($data); - return new ApiBucket(['token' => $identity->getToken()]); - } + $identity = $service->createIdentity($request->getParsedBody()); - protected function validateToken($token): void - { - if (!is_string($token)) { - throw new \InvalidArgumentException('Token should be string.'); - } - if (strlen($token) !== 128) { - throw new \InvalidArgumentException('Invalid token.'); - } + return ['token' => $identity->getToken()]; } } diff --git a/src/Api/UI/Controller/ContactController.php b/src/Api/UI/Controller/ContactController.php index 13e5d6f..a561802 100644 --- a/src/Api/UI/Controller/ContactController.php +++ b/src/Api/UI/Controller/ContactController.php @@ -4,7 +4,7 @@ namespace App\Api\UI\Controller; -use App\Form\ContactForm; +use App\Api\UI\Form\ContactForm; use App\Module\Contact\Api\ContactMailer; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; diff --git a/src/Api/UI/Form/ContactForm.php b/src/Api/UI/Form/ContactForm.php index 49629c1..c45ec95 100644 --- a/src/Api/UI/Form/ContactForm.php +++ b/src/Api/UI/Form/ContactForm.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace App\Form; +namespace App\Api\UI\Form; use Yiisoft\Form\FormModel; use Yiisoft\Validator\Rule\Email; diff --git a/src/Common/Application/AuthRequestErrorHandler.php b/src/Common/Application/AuthRequestErrorHandler.php new file mode 100644 index 0000000..8b7f067 --- /dev/null +++ b/src/Common/Application/AuthRequestErrorHandler.php @@ -0,0 +1,58 @@ +smartStreamFactory = $smartStreamFactory; + $this->responseFactory = $responseFactory; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + return $this->createResponse($this->createBody()); + } + + private function createBody(): StreamInterface + { + return $this->smartStreamFactory->createStream($this->createErrorBucket()); + } + + private function createResponse(StreamInterface $stream): ResponseInterface + { + return $this->responseFactory + ->createResponse(Status::UNAUTHORIZED) + ->withBody($stream) + ->withHeader(Header::CONTENT_TYPE, 'application/json'); + } + + private function createErrorBucket(): DataBucket + { + return new ErrorBucket($this->createError()); + } + + private function createError(): Exception + { + return new HttpException(Status::UNAUTHORIZED, 'Your request was made with invalid credentials.'); + } +} diff --git a/src/Common/Application/Provider/MiddlewareProvider.php b/src/Common/Application/Provider/MiddlewareProvider.php index 55106a9..fc9fac8 100644 --- a/src/Common/Application/Provider/MiddlewareProvider.php +++ b/src/Common/Application/Provider/MiddlewareProvider.php @@ -4,7 +4,11 @@ namespace App\Common\Application\Provider; +use App\Common\Application\AuthRequestErrorHandler; use Psr\Container\ContainerInterface; +use Psr\Http\Message\ResponseFactoryInterface; +use Yiisoft\Auth\AuthInterface; +use Yiisoft\Auth\Middleware\Auth; use Yiisoft\Di\Container; use Yiisoft\Di\Support\ServiceProvider; use Yiisoft\Router\Middleware\Router; @@ -18,13 +22,24 @@ final class MiddlewareProvider extends ServiceProvider */ public function register(Container $container): void { - $container->set(MiddlewareDispatcher::class, static function (ContainerInterface $container) { - return (new MiddlewareDispatcher($container)) - ->addMiddleware($container->get(Router::class)) - // ->addMiddleware($container->get(Yiisoft\Yii\Web\Middleware\SubFolder::class)) - // ->addMiddleware($container->get(Yiisoft\Yii\Web\Session\SessionMiddleware::class)) - // ->addMiddleware($container->get(Yiisoft\Yii\Web\Middleware\Csrf::class)) - ->addMiddleware($container->get(ErrorCatcher::class)); - }); + $container->set( + MiddlewareDispatcher::class, + static function (ContainerInterface $container) { + return (new MiddlewareDispatcher($container)) + ->addMiddleware($container->get(Router::class)) + ->addMiddleware($container->get(ErrorCatcher::class)); + } + ); + + $container->set( + Auth::class, + static function (ContainerInterface $container) { + return new Auth( + $container->get(AuthInterface::class), + $container->get(ResponseFactoryInterface::class), + $container->get(AuthRequestErrorHandler::class) + ); + } + ); } } diff --git a/src/Common/Domain/Exception/EntityNotFound.php b/src/Common/Domain/Exception/EntityNotFound.php new file mode 100644 index 0000000..17b027e --- /dev/null +++ b/src/Common/Domain/Exception/EntityNotFound.php @@ -0,0 +1,9 @@ +created_at = new DateTimeImmutable(); + $this->updated_at = new DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getUrl(): string + { + return $this->url; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function setUrl(string $url): void + { + $this->url = $url; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function setIdentity(Identity $identity): void + { + $this->identity = $identity; + } + + public function getIdentity(): ?Identity + { + return $this->identity; + } +} diff --git a/src/Module/Link/Domain/Mapper/LinkMapper.php b/src/Module/Link/Domain/Mapper/LinkMapper.php new file mode 100644 index 0000000..80bce05 --- /dev/null +++ b/src/Module/Link/Domain/Mapper/LinkMapper.php @@ -0,0 +1,41 @@ +register('created_at', new \DateTimeImmutable(), true); + $command->register('created_at', new \DateTimeImmutable(), true); + + $state->register('updated_at', new \DateTimeImmutable(), true); + $command->register('updated_at', new \DateTimeImmutable(), true); + + return $command; + } + + /** + * @suppress PhanUndeclaredMethod + */ + public function queueUpdate($entity, Node $node, State $state): ContextCarrierInterface + { + /** @var Update $command */ + $command = parent::queueUpdate($entity, $node, $state); + + $state->register('updated_at', new \DateTimeImmutable(), true); + $command->registerAppendix('updated_at', new \DateTimeImmutable()); + + return $command; + } +} diff --git a/src/Module/Link/Domain/Repository/LinkRepository.php b/src/Module/Link/Domain/Repository/LinkRepository.php new file mode 100644 index 0000000..a1cc00c --- /dev/null +++ b/src/Module/Link/Domain/Repository/LinkRepository.php @@ -0,0 +1,45 @@ +orm = $orm; + parent::__construct($select); + } + + public function findOneByUrlAndIdentity(string $url, Identity $identity): ?Link + { + return $this->findOne(['url' => $url, 'identity_id' => $identity->getId()]); + } + + public function save(Link $link): void + { + $transaction = new Transaction($this->orm); + $transaction->persist($link); + $transaction->run(); + } + + public function delete(Link $link): void + { + $transaction = new Transaction($this->orm); + $transaction->delete($link); + $transaction->run(); + } +} diff --git a/src/Module/Link/Domain/Validation/CreateLinkForm.php b/src/Module/Link/Domain/Validation/CreateLinkForm.php new file mode 100644 index 0000000..e585370 --- /dev/null +++ b/src/Module/Link/Domain/Validation/CreateLinkForm.php @@ -0,0 +1,45 @@ +url; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function hasDescription(): bool + { + return $this->description !== null; + } + + protected function rules(): array + { + return [ + 'url' => [ + new Required(), + new Url(), + (new HasLength())->max(255) + ], + 'description' => [ + (new HasLength())->max(255)->skipOnEmpty(true) + ], + ]; + } +} diff --git a/src/Module/Link/Domain/Validation/FindLinkForm.php b/src/Module/Link/Domain/Validation/FindLinkForm.php new file mode 100644 index 0000000..49ccae3 --- /dev/null +++ b/src/Module/Link/Domain/Validation/FindLinkForm.php @@ -0,0 +1,31 @@ +url; + } + + protected function rules(): array + { + return [ + 'url' => [ + new Required(), + new Url(), + (new HasLength())->max(255) + ], + ]; + } +} diff --git a/src/Module/Link/Service/UserLink.php b/src/Module/Link/Service/UserLink.php new file mode 100644 index 0000000..10b7374 --- /dev/null +++ b/src/Module/Link/Service/UserLink.php @@ -0,0 +1,54 @@ +repository = $repository; + } + + public function createLink(CreateLinkForm $form, Identity $identity): Link + { + $link = new Link(); + $link->setUrl($form->getUrl()); + + if ($form->hasDescription()) { + $link->setDescription($form->getDescription()); + } + + $link->setIdentity($identity); + $this->repository->save($link); + + return $link; + } + + public function deleteLink(string $url, Identity $identity): void + { + $this->repository->delete( + $this->getLink($url, $identity) + ); + } + + public function getLink(string $url, Identity $identity): Link + { + $link = $this->repository->findOneByUrlAndIdentity($url, $identity); + if ($link === null) { + throw new EntityNotFound('Url not found.'); + } + + return $link; + } +}