diff --git a/composer.development.json b/composer.development.json index c5988a6..22376ae 100644 --- a/composer.development.json +++ b/composer.development.json @@ -27,6 +27,7 @@ "MyTube\\API\\Console\\": "src\/ApiDomain\/Console\/src", "MyTube\\API\\External\\Authentication\\": "src\/ApiDomain\/External\/Authentication\/src", "MyTube\\API\\External\\Health\\": "src\/ApiDomain\/External\/Health\/src", + "MyTube\\API\\External\\Tag\\": "src\/ApiDomain\/External\/Tag\/src", "MyTube\\API\\External\\User\\": "src\/ApiDomain\/External\/User\/src", "MyTube\\API\\External\\Video\\": "src\/ApiDomain\/External\/Video\/src", "MyTube\\API\\External\\VideoList\\": "src\/ApiDomain\/External\/VideoList\/src", @@ -34,6 +35,7 @@ "MyTube\\Data\\Log\\": "src\/DataDomain\/Log\/src", "MyTube\\Handling\\Registration\\": "src\/HandlingDomain\/Registration\/src", "MyTube\\Handling\\Role\\": "src\/HandlingDomain\/Role\/src", + "MyTube\\Handling\\Tag\\": "src\/HandlingDomain\/Tag\/src", "MyTube\\Handling\\User\\": "src\/HandlingDomain\/User\/src", "MyTube\\Handling\\UserSession\\": "src\/HandlingDomain\/UserSession\/src", "MyTube\\Handling\\Video\\": "src\/HandlingDomain\/Video\/src", @@ -95,6 +97,7 @@ "psr-4": { "MyTube\\API\\External\\Authentication\\": "src\/ApiDomain\/External\/Authentication\/src", "MyTube\\API\\External\\Health\\": "src\/ApiDomain\/External\/Health\/src", + "MyTube\\API\\External\\Tag\\": "src\/ApiDomain\/External\/Tag\/src", "MyTube\\API\\External\\User\\": "src\/ApiDomain\/External\/User\/src", "MyTube\\API\\External\\Video\\": "src\/ApiDomain\/External\/Video\/src", "MyTube\\API\\External\\VideoList\\": "src\/ApiDomain\/External\/VideoList\/src", @@ -102,6 +105,7 @@ "MyTube\\Data\\Log\\": "src\/DataDomain\/Log\/src", "MyTube\\Handling\\Registration\\": "src\/HandlingDomain\/Registration\/src", "MyTube\\Handling\\Role\\": "src\/HandlingDomain\/Role\/src", + "MyTube\\Handling\\Tag\\": "src\/HandlingDomain\/Tag\/src", "MyTube\\Handling\\User\\": "src\/HandlingDomain\/User\/src", "MyTube\\Handling\\UserSession\\": "src\/HandlingDomain\/UserSession\/src", "MyTube\\Handling\\Video\\": "src\/HandlingDomain\/Video\/src", diff --git a/config/config.php b/config/config.php index 6302238..80c5026 100644 --- a/config/config.php +++ b/config/config.php @@ -59,6 +59,7 @@ $aggregator = new ConfigAggregator([ \MyTube\Handling\Registration\ConfigProvider::class, \MyTube\Handling\Video\ConfigProvider::class, \MyTube\Handling\VideoList\ConfigProvider::class, + \MyTube\Handling\Tag\ConfigProvider::class, // API /// Command @@ -70,6 +71,7 @@ $aggregator = new ConfigAggregator([ \MyTube\API\External\Authentication\ConfigProvider::class, \MyTube\API\External\Video\ConfigProvider::class, \MyTube\API\External\VideoList\ConfigProvider::class, + \MyTube\API\External\Tag\ConfigProvider::class, /// Internal diff --git a/data/migrations/myTube/Version20240223130626.php b/data/migrations/myTube/Version20240223130626.php index 9771a93..4352b9b 100644 --- a/data/migrations/myTube/Version20240223130626.php +++ b/data/migrations/myTube/Version20240223130626.php @@ -14,18 +14,24 @@ final class Version20240223130626 extends AbstractMigration { public function getDescription(): string { - return ''; + return "Create Table 'tag'"; } public function up(Schema $schema): void { - // this up() migration is auto-generated, please modify it to your needs + $sql = "CREATE TABLE tag ( + id binary(16) NOT NULL, + description varchar(255) NOT NULL, + created_at datetime NOT NULL, + updated_at datetime NOT NULL, + PRIMARY KEY (id) + );"; + $this->addSql($sql); } public function down(Schema $schema): void { - // this down() migration is auto-generated, please modify it to your needs - + $this->addSql("DROP TABLE tag;"); } } diff --git a/data/migrations/myTube/Version20240223130835.php b/data/migrations/myTube/Version20240223130835.php new file mode 100644 index 0000000..917cb80 --- /dev/null +++ b/data/migrations/myTube/Version20240223130835.php @@ -0,0 +1,38 @@ +addSql($sql); + } + + public function down(Schema $schema): void + { + $this->addSql("DROP TABLE tag_alias;"); + } +} diff --git a/data/migrations/myTube/Version20240223142111.php b/data/migrations/myTube/Version20240223142111.php new file mode 100644 index 0000000..06b8963 --- /dev/null +++ b/data/migrations/myTube/Version20240223142111.php @@ -0,0 +1,35 @@ +addSql($sql); + } + + public function down(Schema $schema): void + { + $this->addSql("DROP TABLE video_tag;"); + } +} diff --git a/docker/nginx/config/nginx.conf b/docker/nginx/config/nginx.conf index e10eefe..cd11c50 100644 --- a/docker/nginx/config/nginx.conf +++ b/docker/nginx/config/nginx.conf @@ -5,7 +5,7 @@ upstream host-backend-app { server { listen 80 default_server; - client_max_body_size 1000M; + client_max_body_size 10000M; location / { fastcgi_pass host-backend-app; diff --git a/docker/php/config/uploads.ini b/docker/php/config/uploads.ini index f8837bf..0ded08e 100644 --- a/docker/php/config/uploads.ini +++ b/docker/php/config/uploads.ini @@ -1,5 +1,5 @@ file_uploads = On -memory_limit = 1000M -upload_max_filesize = 1000M -post_max_size = 1000M -max_execution_time = 600 \ No newline at end of file +memory_limit = 10000M +upload_max_filesize = 10000M +post_max_size = 10000M +max_execution_time = 1000 \ No newline at end of file diff --git a/src/ApiDomain/Console/config/console.php b/src/ApiDomain/Console/config/console.php index 7c6b749..de724cb 100644 --- a/src/ApiDomain/Console/config/console.php +++ b/src/ApiDomain/Console/config/console.php @@ -1,5 +1,6 @@ [ InitializeDataCommand::class, RbacUpdateCommand::class, + AnalyzeVideoTitlesCommand::class, ] ]; diff --git a/src/ApiDomain/Console/config/service_manager.php b/src/ApiDomain/Console/config/service_manager.php index 00ff486..77e3fcc 100644 --- a/src/ApiDomain/Console/config/service_manager.php +++ b/src/ApiDomain/Console/config/service_manager.php @@ -1,5 +1,6 @@ [ InitializeDataCommand::class => AutoWiringFactory::class, RbacUpdateCommand::class => AutoWiringFactory::class, + AnalyzeVideoTitlesCommand::class => AutoWiringFactory::class, ], ]; diff --git a/src/ApiDomain/Console/src/Command/AnalyzeVideoTitlesCommand.php b/src/ApiDomain/Console/src/Command/AnalyzeVideoTitlesCommand.php new file mode 100644 index 0000000..e65824f --- /dev/null +++ b/src/ApiDomain/Console/src/Command/AnalyzeVideoTitlesCommand.php @@ -0,0 +1,66 @@ +getName()); + + $this->videoRepository = $this->entityManager->getRepository(Video::class); + } + + protected function execute( + InputInterface $input, + OutputInterface $output + ): int { + $io = new SymfonyStyle($input, $output); + + try { + $videos = $this->videoRepository->findAll(); + + /** @var Video $video */ + foreach ($videos as $video) { + $io->info($video->getTitle()); + + $matches = $this->analyzer->analyze($video); + + /** @var Tag $match */ + foreach ($matches as $match) { + $io->info(sprintf(" - added Tag '%s'", $match->getDescription())); + } + + $this->entityManager->persist($video); + $this->entityManager->flush(); + } + + $io->success('OK!'); + } catch (\Throwable $e) { + $io->error($e->getMessage()); + $io->error($e->getTraceAsString()); + $this->logger->error($e->getMessage(), ['exception' => $e]); + return Command::FAILURE; + } + + return Command::SUCCESS; + } +} diff --git a/src/ApiDomain/External/Tag/config/routes.php b/src/ApiDomain/External/Tag/config/routes.php new file mode 100644 index 0000000..e3b172a --- /dev/null +++ b/src/ApiDomain/External/Tag/config/routes.php @@ -0,0 +1,23 @@ + 'tag.create', + 'path' => '/api/tag/create[/]', + 'allowed_methods' => ['POST'], + 'middleware' => [ + CreateHandler::class, + ], + ], + [ + 'name' => 'tag.add-alias', + 'path' => '/api/tag/add-alias[/]', + 'allowed_methods' => ['POST'], + 'middleware' => [ + AddAliasHandler::class, + ], + ], +]; diff --git a/src/ApiDomain/External/Tag/config/service_manager.php b/src/ApiDomain/External/Tag/config/service_manager.php new file mode 100644 index 0000000..4a6c866 --- /dev/null +++ b/src/ApiDomain/External/Tag/config/service_manager.php @@ -0,0 +1,19 @@ + [ + // Handler + AddAliasHandler::class => AutoWiringFactory::class, + CreateHandler::class => AutoWiringFactory::class, + + // Response Formatter + AddAliasResponseFormatter::class => AutoWiringFactory::class, + CreateResponseFormatter::class => AutoWiringFactory::class, + ], +]; diff --git a/src/ApiDomain/External/Tag/src/ConfigProvider.php b/src/ApiDomain/External/Tag/src/ConfigProvider.php new file mode 100644 index 0000000..e624ab4 --- /dev/null +++ b/src/ApiDomain/External/Tag/src/ConfigProvider.php @@ -0,0 +1,16 @@ + require __DIR__ . './../config/service_manager.php', + 'routes' => require __DIR__ . '/./../config/routes.php', + ]; + } +} diff --git a/src/ApiDomain/External/Tag/src/Handler/AddAliasHandler.php b/src/ApiDomain/External/Tag/src/Handler/AddAliasHandler.php new file mode 100644 index 0000000..06937ed --- /dev/null +++ b/src/ApiDomain/External/Tag/src/Handler/AddAliasHandler.php @@ -0,0 +1,46 @@ +getBody()->getContents(), + true + ); + + $addAliasCommand = $this->addAliasCommandBuilder->build( + Uuid::fromString($data['tagId']), + $data['description'] + ); + $result = $this->addAliasCommandHandler->execute($addAliasCommand); + + return new SuccessResponse($this->responseFormatter->format($result)); + } +} diff --git a/src/ApiDomain/External/Tag/src/Handler/CreateHandler.php b/src/ApiDomain/External/Tag/src/Handler/CreateHandler.php new file mode 100644 index 0000000..1bac6c9 --- /dev/null +++ b/src/ApiDomain/External/Tag/src/Handler/CreateHandler.php @@ -0,0 +1,42 @@ +getBody()->getContents(), + true + ); + + $createCommand = $this->createCommandBuilder->build( + $data['description'] + ); + $result = $this->createCommandHandler->execute($createCommand); + + return new SuccessResponse($this->responseFormatter->format($result)); + } +} diff --git a/src/ApiDomain/External/Tag/src/ResponseFormatter/AddAliasResponseFormatter.php b/src/ApiDomain/External/Tag/src/ResponseFormatter/AddAliasResponseFormatter.php new file mode 100644 index 0000000..d405e83 --- /dev/null +++ b/src/ApiDomain/External/Tag/src/ResponseFormatter/AddAliasResponseFormatter.php @@ -0,0 +1,25 @@ +getAlias(); + $tag = $alias->getTag(); + + return [ + 'id' => $alias->getId()->toString(), + 'description' => $alias->getDescription(), + 'tag' => [ + 'id' => $tag->getId()->toString(), + 'description' => $tag->getDescription() + ], + ]; + } +} diff --git a/src/ApiDomain/External/Tag/src/ResponseFormatter/CreateResponseFormatter.php b/src/ApiDomain/External/Tag/src/ResponseFormatter/CreateResponseFormatter.php new file mode 100644 index 0000000..0b0a729 --- /dev/null +++ b/src/ApiDomain/External/Tag/src/ResponseFormatter/CreateResponseFormatter.php @@ -0,0 +1,19 @@ +getTag(); + return [ + 'id' => $tag->getId()->toString(), + 'description' => $tag->getDescription(), + ]; + } +} diff --git a/src/ApiDomain/External/VideoList/config/routes.php b/src/ApiDomain/External/VideoList/config/routes.php index 7d9717f..cde928e 100644 --- a/src/ApiDomain/External/VideoList/config/routes.php +++ b/src/ApiDomain/External/VideoList/config/routes.php @@ -1,6 +1,7 @@ 'video-list.upload', + 'path' => '/api/video-list/upload[/]', + 'allowed_methods' => ['POST'], + 'middleware' => [ + UploadHandler::class + ], + ], ]; diff --git a/src/ApiDomain/External/VideoList/config/service_manager.php b/src/ApiDomain/External/VideoList/config/service_manager.php index c1b5eff..356dd8e 100644 --- a/src/ApiDomain/External/VideoList/config/service_manager.php +++ b/src/ApiDomain/External/VideoList/config/service_manager.php @@ -1,15 +1,17 @@ [ ReadListHandler::class => AutoWiringFactory::class, + UploadHandler::class => AutoWiringFactory::class, ReadListResponseFormatter::class => AutoWiringFactory::class, + UploadResponseFormatter::class => AutoWiringFactory::class, ], ]; diff --git a/src/ApiDomain/External/VideoList/src/Handler/UploadHandler.php b/src/ApiDomain/External/VideoList/src/Handler/UploadHandler.php new file mode 100644 index 0000000..37461a1 --- /dev/null +++ b/src/ApiDomain/External/VideoList/src/Handler/UploadHandler.php @@ -0,0 +1,41 @@ +getUploadedFiles(); + + if (count($uploadedFiles) < 1) { + return new JsonResponse('Upload at least one File', Response::STATUS_CODE_400); + } + + $uploadCommand = $this->uploadCommandBuilder->build( + $request->getUploadedFiles() + ); + $result = $this->uploadCommandHandler->execute($uploadCommand); + + return new SuccessResponse($this->responseFormatter->format($result)); + } +} diff --git a/src/ApiDomain/External/VideoList/src/ResponseFormatter/ReadListResponseFormatter.php b/src/ApiDomain/External/VideoList/src/ResponseFormatter/ReadListResponseFormatter.php index 6d1a415..ce67090 100644 --- a/src/ApiDomain/External/VideoList/src/ResponseFormatter/ReadListResponseFormatter.php +++ b/src/ApiDomain/External/VideoList/src/ResponseFormatter/ReadListResponseFormatter.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace MyTube\API\External\VideoList\ResponseFormatter; +use MyTube\Data\Business\Entity\Tag; use MyTube\Data\Business\Entity\Video; class ReadListResponseFormatter @@ -14,9 +15,17 @@ class ReadListResponseFormatter /** @var Video $video */ foreach ($videos as $video) { + $tags = []; + + /** @var Tag $tag */ + foreach ($video->getTags() as $tag) { + $tags[] = $tag->getDescription(); + } + $result[] = [ 'title' => $video->getTitle(), 'id' => $video->getId()->toString(), + 'tags' => $tags ]; } diff --git a/src/ApiDomain/External/VideoList/src/ResponseFormatter/UploadResponseFormatter.php b/src/ApiDomain/External/VideoList/src/ResponseFormatter/UploadResponseFormatter.php new file mode 100644 index 0000000..0286d8b --- /dev/null +++ b/src/ApiDomain/External/VideoList/src/ResponseFormatter/UploadResponseFormatter.php @@ -0,0 +1,46 @@ +getUploadedFileResults(); + $details = []; + + $successCount = 0; + $failCount = 0; + + /** @var UploadedFileResult $uploadedFileResult */ + foreach ($uploadedFileResults as $uploadedFileResult) { + + $detail = [ + 'file' => $uploadedFileResult->getUploadedFile()->getClientFilename(), + 'success' => $uploadedFileResult->getSuccess(), + ]; + + if ($uploadedFileResult->getSuccess()) { + $detail['id'] = $uploadedFileResult->getVideo()->getId()->toString(); + $successCount++; + } else { + $detail['error'] = $uploadedFileResult->getException()->getMessage(); + $failCount++; + } + + $details[] = $detail; + } + + return [ + 'total' => count($uploadedFileResults), + 'failed' => $failCount, + 'succeeded' => $successCount, + 'details' => $details + ]; + } +} diff --git a/src/DataDomain/Business/src/Entity/Tag.php b/src/DataDomain/Business/src/Entity/Tag.php new file mode 100644 index 0000000..4b0aa22 --- /dev/null +++ b/src/DataDomain/Business/src/Entity/Tag.php @@ -0,0 +1,119 @@ +id = UuidGenerator::generate(); + + $this->aliases = new ArrayCollection(); + $this->videos = new ArrayCollection(); + + $now = new DateTime(); + $this->setCreatedAt($now); + $this->setUpdatedAt($now); + } + + public function getId(): UuidInterface { + return $this->id; + } + + public function getDescription(): string { + return $this->description; + } + public function setDescription(string $description): void { + $this->description = $description; + } + + public function getVideos(): Collection + { + return $this->videos; + } + public function setVideos(Collection $videos): void + { + $this->videos = $videos; + } + public function addVideo(Video $video): void + { + if (!$this->videos->contains($video)) { + $this->videos->add($video); + } + } + public function removeVideo(Video $video): void + { + if ($this->videos->contains($video)) { + $this->videos->removeElement($video); + } + } + + public function getAliases(): Collection + { + return $this->aliases; + } + public function setAliases(Collection $aliases): void + { + $this->aliases = $aliases; + } + public function addAlias(TagAlias $alias): void + { + if (!$this->aliases->contains($alias)) { + $this->aliases->add($alias); + } + } + public function removeAlias(TagAlias $alias): void + { + if ($this->aliases->contains($alias)) { + $this->aliases->removeElement($alias); + } + } + + public function getCreatedAt(): DateTime { + return $this->createdAt; + } + public function setCreatedAt(DateTime $createdAt): void { + $this->createdAt = $createdAt; + } + + public function getUpdatedAt(): DateTime { + return $this->updatedAt; + } + public function setUpdatedAt(DateTime $updatedAt): void { + $this->updatedAt = $updatedAt; + } +} diff --git a/src/DataDomain/Business/src/Entity/TagAlias.php b/src/DataDomain/Business/src/Entity/TagAlias.php new file mode 100644 index 0000000..9cc0a28 --- /dev/null +++ b/src/DataDomain/Business/src/Entity/TagAlias.php @@ -0,0 +1,90 @@ +id = UuidGenerator::generate(); + + $now = new DateTime(); + $this->setCreatedAt($now); + $this->setUpdatedAt($now); + } + + public function getId(): UuidInterface { + return $this->id; + } + + public function getTagId(): UuidInterface + { + return $this->tagId; + } + public function setTagId(UuidInterface $tagId): void + { + $this->tagId = $tagId; + } + + public function getTag(): Tag + { + return $this->tag; + } + public function setTag(Tag $tag): void + { + $this->tag = $tag; + } + + public function getDescription(): string { + return $this->description; + } + public function setDescription(string $description): void { + $this->description = $description; + } + + public function getCreatedAt(): DateTime { + return $this->createdAt; + } + public function setCreatedAt(DateTime $createdAt): void { + $this->createdAt = $createdAt; + } + + public function getUpdatedAt(): DateTime { + return $this->updatedAt; + } + public function setUpdatedAt(DateTime $updatedAt): void { + $this->updatedAt = $updatedAt; + } +} diff --git a/src/DataDomain/Business/src/Entity/Video.php b/src/DataDomain/Business/src/Entity/Video.php index dd6cd04..b21a565 100644 --- a/src/DataDomain/Business/src/Entity/Video.php +++ b/src/DataDomain/Business/src/Entity/Video.php @@ -3,6 +3,8 @@ namespace MyTube\Data\Business\Entity; use DateTime; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use MyTube\Infrastructure\UuidGenerator\UuidGenerator; use Ramsey\Uuid\UuidInterface; @@ -24,6 +26,12 @@ class Video { /** @ORM\Column(name="directory_path", type="string") */ private ?string $directoryPath; + /** + * @ORM\ManyToMany(targetEntity="Tag", inversedBy="videos") + * @ORM\JoinTable(name="video_tag") + */ + private Collection $tags; + /** @ORM\Column(name="created_at", type="datetime") */ private DateTime $createdAt; @@ -33,7 +41,9 @@ class Video { public function __construct() { $this->id = UuidGenerator::generate(); - + + $this->tags = new ArrayCollection(); + $now = new DateTime(); $this->setCreatedAt($now); $this->setUpdatedAt($now); @@ -47,7 +57,6 @@ class Video { public function getTitle(): string { return $this->title; } - public function setTitle(string $title): void { $this->title = $title; } @@ -55,15 +64,34 @@ class Video { public function getDirectoryPath(): ?string { return $this->directoryPath; } - public function setDirectoryPath(?string $directoryPath): void { $this->directoryPath = $directoryPath; } + public function getTags(): Collection + { + return $this->tags; + } + public function setTags(Collection $tags): void + { + $this->tags = $tags; + } + public function addTag(Tag $tag): void + { + if (!$this->tags->contains($tag)) { + $this->tags->add($tag); + } + } + public function removeTag(Tag $tag): void + { + if ($this->tags->contains($tag)) { + $this->tags->removeElement($tag); + } + } + public function getCreatedAt(): DateTime { return $this->createdAt; } - public function setCreatedAt(DateTime $createdAt): void { $this->createdAt = $createdAt; } @@ -71,7 +99,6 @@ class Video { public function getUpdatedAt(): DateTime { return $this->updatedAt; } - public function setUpdatedAt(DateTime $updatedAt): void { $this->updatedAt = $updatedAt; } diff --git a/src/DataDomain/Business/src/Repository/TagAliasRepository.php b/src/DataDomain/Business/src/Repository/TagAliasRepository.php new file mode 100644 index 0000000..2be07a0 --- /dev/null +++ b/src/DataDomain/Business/src/Repository/TagAliasRepository.php @@ -0,0 +1,8 @@ + [ + + /// Rule + TagExistsRule::class => InjectionFactory::class, + TagAliasExistsRule::class => InjectionFactory::class, + + /// Builder + TagBuilder::class => AutoWiringFactory::class, + TagAliasBuilder::class => AutoWiringFactory::class, + + /// CQRS + // Add Alias + AddAliasCommandBuilder::class => AutoWiringFactory::class, + AddAliasCommandHandler::class => InjectionFactory::class, + // Create + CreateCommandBuilder::class => AutoWiringFactory::class, + CreateCommandHandler::class => AutoWiringFactory::class, + ], +]; diff --git a/src/HandlingDomain/Tag/src/Builder/TagAliasBuilder.php b/src/HandlingDomain/Tag/src/Builder/TagAliasBuilder.php new file mode 100644 index 0000000..fc9227a --- /dev/null +++ b/src/HandlingDomain/Tag/src/Builder/TagAliasBuilder.php @@ -0,0 +1,21 @@ +setTag($tag); + $tagAlias->setDescription($description); + + return $tagAlias; + } +} diff --git a/src/HandlingDomain/Tag/src/Builder/TagBuilder.php b/src/HandlingDomain/Tag/src/Builder/TagBuilder.php new file mode 100644 index 0000000..c40a076 --- /dev/null +++ b/src/HandlingDomain/Tag/src/Builder/TagBuilder.php @@ -0,0 +1,18 @@ +setDescription($description); + + return $tag; + } +} diff --git a/src/HandlingDomain/Tag/src/ConfigProvider.php b/src/HandlingDomain/Tag/src/ConfigProvider.php new file mode 100644 index 0000000..7ce7c51 --- /dev/null +++ b/src/HandlingDomain/Tag/src/ConfigProvider.php @@ -0,0 +1,15 @@ + require __DIR__ . '/./../config/service_manager.php', + ]; + } +} diff --git a/src/HandlingDomain/Tag/src/Exception/TagAliasAlreadyExistsException.php b/src/HandlingDomain/Tag/src/Exception/TagAliasAlreadyExistsException.php new file mode 100644 index 0000000..82e585c --- /dev/null +++ b/src/HandlingDomain/Tag/src/Exception/TagAliasAlreadyExistsException.php @@ -0,0 +1,25 @@ +toString() + ), + ErrorDomain::Tag, + ErrorCode::NotFound + ); + } +} diff --git a/src/HandlingDomain/Tag/src/Handler/Command/AddAlias/AddAliasCommand.php b/src/HandlingDomain/Tag/src/Handler/Command/AddAlias/AddAliasCommand.php new file mode 100644 index 0000000..fae07e5 --- /dev/null +++ b/src/HandlingDomain/Tag/src/Handler/Command/AddAlias/AddAliasCommand.php @@ -0,0 +1,26 @@ +tagId; + } + + public function getDescription(): string + { + return $this->description; + } +} diff --git a/src/HandlingDomain/Tag/src/Handler/Command/AddAlias/AddAliasCommandBuilder.php b/src/HandlingDomain/Tag/src/Handler/Command/AddAlias/AddAliasCommandBuilder.php new file mode 100644 index 0000000..dd4617e --- /dev/null +++ b/src/HandlingDomain/Tag/src/Handler/Command/AddAlias/AddAliasCommandBuilder.php @@ -0,0 +1,20 @@ +getTagId(); + $description = $addAliasCommand->getDescription(); + + if ($this->existsRule->appliesTo($description)) { + throw new TagAliasAlreadyExistsException($description); + } + + $tag = $this->tagRepository->findOneBy(['id' => $tagId]); + if ($tag === null) { + throw new TagNotFoundByIdException($tagId); + } + + $tagAlias = $this->aliasBuilder->build( + $tag, + $description + ); + + $this->entityManager->persist($tagAlias); + $this->entityManager->flush(); + + return new AddAliasCommandResult($tagAlias); + } +} diff --git a/src/HandlingDomain/Tag/src/Handler/Command/AddAlias/AddAliasCommandResult.php b/src/HandlingDomain/Tag/src/Handler/Command/AddAlias/AddAliasCommandResult.php new file mode 100644 index 0000000..734c2ff --- /dev/null +++ b/src/HandlingDomain/Tag/src/Handler/Command/AddAlias/AddAliasCommandResult.php @@ -0,0 +1,20 @@ +alias; + } +} diff --git a/src/HandlingDomain/Tag/src/Handler/Command/Create/CreateCommand.php b/src/HandlingDomain/Tag/src/Handler/Command/Create/CreateCommand.php new file mode 100644 index 0000000..0d56fe1 --- /dev/null +++ b/src/HandlingDomain/Tag/src/Handler/Command/Create/CreateCommand.php @@ -0,0 +1,18 @@ +description; + } +} diff --git a/src/HandlingDomain/Tag/src/Handler/Command/Create/CreateCommandBuilder.php b/src/HandlingDomain/Tag/src/Handler/Command/Create/CreateCommandBuilder.php new file mode 100644 index 0000000..72d4acd --- /dev/null +++ b/src/HandlingDomain/Tag/src/Handler/Command/Create/CreateCommandBuilder.php @@ -0,0 +1,16 @@ +getDescription(); + + if ($this->existsRule->appliesTo($description)) { + throw new TagAlreadyExistsException($description); + } + + $tag = $this->tagBuilder->build($description); + + $this->entityManager->persist($tag); + $this->entityManager->flush(); + + return new CreateCommandResult($tag); + } +} diff --git a/src/HandlingDomain/Tag/src/Handler/Command/Create/CreateCommandResult.php b/src/HandlingDomain/Tag/src/Handler/Command/Create/CreateCommandResult.php new file mode 100644 index 0000000..66db0da --- /dev/null +++ b/src/HandlingDomain/Tag/src/Handler/Command/Create/CreateCommandResult.php @@ -0,0 +1,20 @@ +tag; + } +} diff --git a/src/HandlingDomain/Tag/src/Rule/TagAliasExistsRule.php b/src/HandlingDomain/Tag/src/Rule/TagAliasExistsRule.php new file mode 100644 index 0000000..919d241 --- /dev/null +++ b/src/HandlingDomain/Tag/src/Rule/TagAliasExistsRule.php @@ -0,0 +1,27 @@ +tagAliasRepository->findOneBy(['description' => $description]) !== null; + } +} diff --git a/src/HandlingDomain/Tag/src/Rule/TagExistsRule.php b/src/HandlingDomain/Tag/src/Rule/TagExistsRule.php new file mode 100644 index 0000000..19da123 --- /dev/null +++ b/src/HandlingDomain/Tag/src/Rule/TagExistsRule.php @@ -0,0 +1,28 @@ +tagRepository->findOneBy(['description' => $description]) !== null; + } +} diff --git a/src/HandlingDomain/Video/config/service_manager.php b/src/HandlingDomain/Video/config/service_manager.php index f94b3f9..0de17ee 100644 --- a/src/HandlingDomain/Video/config/service_manager.php +++ b/src/HandlingDomain/Video/config/service_manager.php @@ -10,6 +10,14 @@ use MyTube\Handling\Video\Handler\Query\Stream\StreamQueryBuilder; use MyTube\Handling\Video\Handler\Query\Stream\StreamQueryHandler; use MyTube\Handling\Video\Handler\Query\Thumbnail\ThumbnailQueryBuilder; use MyTube\Handling\Video\Handler\Query\Thumbnail\ThumbnailQueryHandler; +use MyTube\Handling\Video\Analyzer\VideoTitleAnalyzer; +use MyTube\Handling\Video\Pipeline\Upload\Step\AnalyzeTitleStep; +use MyTube\Handling\Video\Pipeline\Upload\Step\BuildVideoStep; +use MyTube\Handling\Video\Pipeline\Upload\Step\CheckVideoStep; +use MyTube\Handling\Video\Pipeline\Upload\Step\SaveEntityStep; +use MyTube\Handling\Video\Pipeline\Upload\Step\UploadFileStep; +use MyTube\Handling\Video\Pipeline\Upload\UploadPipeline; +use MyTube\Handling\Video\Rule\VideoExistsRule; use MyTube\Handling\Video\Uploader\VideoUploader; use Reinfi\DependencyInjection\Factory\AutoWiringFactory; use Reinfi\DependencyInjection\Factory\InjectionFactory; @@ -17,6 +25,9 @@ use Reinfi\DependencyInjection\Factory\InjectionFactory; return [ 'factories' => [ + /// Uploader + VideoTitleAnalyzer::class => InjectionFactory::class, + /// Uploader VideoUploader::class => AutoWiringFactory::class, @@ -24,6 +35,9 @@ return [ VideoBuilder::class => AutoWiringFactory::class, VideoImageBuilder::class => AutoWiringFactory::class, + /// Rule + VideoExistsRule::class => InjectionFactory::class, + /// CQRS // Stream Query StreamQueryHandler::class => InjectionFactory::class, @@ -37,5 +51,14 @@ return [ // Read Details ReadDetailsQueryHandler::class => InjectionFactory::class, ReadDetailsQueryBuilder::class => AutoWiringFactory::class, + + /// Pipeline + // Upload + CheckVideoStep::class => AutoWiringFactory::class, + UploadPipeline::class => AutoWiringFactory::class, + BuildVideoStep::class => AutoWiringFactory::class, + AnalyzeTitleStep::class => AutoWiringFactory::class, + UploadFileStep::class => AutoWiringFactory::class, + SaveEntityStep::class => AutoWiringFactory::class, ], ]; diff --git a/src/HandlingDomain/Video/src/Analyzer/VideoTitleAnalyzer.php b/src/HandlingDomain/Video/src/Analyzer/VideoTitleAnalyzer.php new file mode 100644 index 0000000..71b0474 --- /dev/null +++ b/src/HandlingDomain/Video/src/Analyzer/VideoTitleAnalyzer.php @@ -0,0 +1,66 @@ +normalizeString($video->getTitle()); + + $tags = $this->tagRepository->findAll(); + $matches = []; + + /** @var Tag $tag */ + foreach ($tags as $tag) { + if (str_contains($title, $this->normalizeString($tag->getDescription()))) { + $matches[] = $tag; + continue; + } + + /** @var TagAlias $alias */ + foreach ($tag->getAliases() as $alias) { + if (str_contains($title, $this->normalizeString($alias->getDescription()))) { + $matches[] = $tag; + break; + } + } + } + + /** @var Tag $match */ + foreach ($matches as $match) { + $video->addTag($match); + } + + return $matches; + } + + private function normalizeString(string $string): string { + return preg_replace( + '/[^a-zA-Z0-9]+/', + '', + strtolower($string) + ); + } +} diff --git a/src/HandlingDomain/Video/src/Builder/VideoImageBuilder.php b/src/HandlingDomain/Video/src/Builder/VideoImageBuilder.php index f3d06b8..ce8dd61 100644 --- a/src/HandlingDomain/Video/src/Builder/VideoImageBuilder.php +++ b/src/HandlingDomain/Video/src/Builder/VideoImageBuilder.php @@ -2,24 +2,27 @@ namespace MyTube\Handling\Video\Builder; -use MyTube\Data\Business\Entity\Video; +use Ramsey\Uuid\UuidInterface; class VideoImageBuilder { private const FILE_NAME = 'thumbnail.png'; public function build( - Video $video, + UuidInterface $directoryId, string $timestamp = '00:00:10' ): string|null { - $videoId = $video->getId()->toString(); - - $targetPath = sprintf( - '%s/%s/%s/%s', + $directoryPath = sprintf( + '%s/%s/%s', APP_ROOT, 'var/filestore', - $videoId, + $directoryId->toString() + ); + + $targetPath = sprintf( + '%s/%s', + $directoryPath, self::FILE_NAME ); @@ -27,11 +30,14 @@ class VideoImageBuilder return null; } - $command = "cd /var/www/html/var/filestore/" . $videoId . "/" . - " && " . - "ffmpeg -i video.mp4 -ss " . $timestamp . " -vframes 1 " . self::FILE_NAME; + $command = sprintf( + "cd %s && ffmpeg -i video.mp4 -ss %s -vframes 1 %s", + $directoryPath, + $timestamp, + self::FILE_NAME + ); - $output = shell_exec($command); + shell_exec($command); return $targetPath; } diff --git a/src/HandlingDomain/Video/src/Exception/VideoAlreadyExistsException.php b/src/HandlingDomain/Video/src/Exception/VideoAlreadyExistsException.php new file mode 100644 index 0000000..bd73a9f --- /dev/null +++ b/src/HandlingDomain/Video/src/Exception/VideoAlreadyExistsException.php @@ -0,0 +1,25 @@ +getClientFilename() + ), + ErrorDomain::Video, + ErrorCode::AlreadyExists + ); + } +} \ No newline at end of file diff --git a/src/HandlingDomain/Video/src/Exception/VideoNotFoundByIdException.php b/src/HandlingDomain/Video/src/Exception/VideoNotFoundByIdException.php index 0585f04..412eb47 100644 --- a/src/HandlingDomain/Video/src/Exception/VideoNotFoundByIdException.php +++ b/src/HandlingDomain/Video/src/Exception/VideoNotFoundByIdException.php @@ -9,7 +9,7 @@ use Ramsey\Uuid\UuidInterface; class VideoNotFoundByIdException extends MyTubeException { - private const MESSAGE = 'The user with the Id %s was not found!'; + private const MESSAGE = 'The Video with the Id %s was not found!'; public function __construct(UuidInterface $id) { diff --git a/src/HandlingDomain/Video/src/Handler/Command/Upload/UploadCommandHandler.php b/src/HandlingDomain/Video/src/Handler/Command/Upload/UploadCommandHandler.php index 8655549..548443a 100644 --- a/src/HandlingDomain/Video/src/Handler/Command/Upload/UploadCommandHandler.php +++ b/src/HandlingDomain/Video/src/Handler/Command/Upload/UploadCommandHandler.php @@ -8,38 +8,26 @@ use MyTube\Data\Business\Entity\Video; use MyTube\Data\Business\Manager\MyTubeEntityManager; use MyTube\Handling\Video\Builder\VideoBuilder; use MyTube\Handling\Video\Builder\VideoImageBuilder; -use MyTube\Handling\Video\Uploader\VideoUploader; +use MyTube\Handling\Video\Pipeline\Upload\UploadPayload; +use MyTube\Handling\Video\Pipeline\Upload\UploadPipeline; +use MyTube\Handling\Video\Uploader\VideoTitleAnalyzer; use Psr\Http\Message\UploadedFileInterface; use Exception; class UploadCommandHandler { public function __construct( - private readonly VideoBuilder $videoBuilder, - private readonly MyTubeEntityManager $entityManager, - private readonly VideoUploader $uploader, + private readonly UploadPipeline $pipeline, ) { } - /** - * @throws Exception - */ public function execute(UploadCommand $uploadCommand): Video { - $uploadedFile = $uploadCommand->getUploadedFile(); + $payload = new UploadPayload(); + $payload->setUploadedFile($uploadCommand->getUploadedFile()); - $video = $this->videoBuilder->build( - $uploadedFile->getClientFilename() - ); + $this->pipeline->handle($payload); - $this->uploader->upload( - $uploadedFile, - $video - ); - - $this->entityManager->persist($video); - $this->entityManager->flush(); - - return $video; + return $payload->getVideo(); } } diff --git a/src/HandlingDomain/Video/src/Handler/Query/ReadDetails/ReadDetailsQueryHandler.php b/src/HandlingDomain/Video/src/Handler/Query/ReadDetails/ReadDetailsQueryHandler.php index 8f7c96e..3678e34 100644 --- a/src/HandlingDomain/Video/src/Handler/Query/ReadDetails/ReadDetailsQueryHandler.php +++ b/src/HandlingDomain/Video/src/Handler/Query/ReadDetails/ReadDetailsQueryHandler.php @@ -18,9 +18,11 @@ class ReadDetailsQueryHandler * entityManager="MyTube\Data\Business\Manager\MyTubeEntityManager", * entity="MyTube\Data\Business\Entity\Video" * ) + * @Inject("MyTube\Handling\Video\Builder\VideoImageBuilder") */ public function __construct( private readonly VideoRepository $videoRepository, + private readonly VideoImageBuilder $videoImageBuilder, ) { } @@ -38,6 +40,8 @@ class ReadDetailsQueryHandler throw new VideoNotFoundByIdException($videoId); } + $this->videoImageBuilder->build($video->getId()); + return new ReadDetailsQueryResult($video); } } diff --git a/src/HandlingDomain/Video/src/Pipeline/Upload/Step/AnalyzeTitleStep.php b/src/HandlingDomain/Video/src/Pipeline/Upload/Step/AnalyzeTitleStep.php new file mode 100644 index 0000000..fa27d6b --- /dev/null +++ b/src/HandlingDomain/Video/src/Pipeline/Upload/Step/AnalyzeTitleStep.php @@ -0,0 +1,33 @@ +getVideo(); + + $this->analyzer->analyze($video); + + $pipeline->next()($payload, $pipeline); + } +} diff --git a/src/HandlingDomain/Video/src/Pipeline/Upload/Step/BuildVideoStep.php b/src/HandlingDomain/Video/src/Pipeline/Upload/Step/BuildVideoStep.php new file mode 100644 index 0000000..650c629 --- /dev/null +++ b/src/HandlingDomain/Video/src/Pipeline/Upload/Step/BuildVideoStep.php @@ -0,0 +1,36 @@ +getUploadedFile(); + + $video = $this->videoBuilder->build( + $uploadedFile->getClientFilename() + ); + + $uploadPayload->setVideo($video); + $pipeline->next()($payload, $pipeline); + } +} diff --git a/src/HandlingDomain/Video/src/Pipeline/Upload/Step/CheckVideoStep.php b/src/HandlingDomain/Video/src/Pipeline/Upload/Step/CheckVideoStep.php new file mode 100644 index 0000000..86352d2 --- /dev/null +++ b/src/HandlingDomain/Video/src/Pipeline/Upload/Step/CheckVideoStep.php @@ -0,0 +1,40 @@ +getUploadedFile(); + + if ($this->existsRule->appliesTo($uploadedFile)) { + throw new VideoAlreadyExistsException($uploadedFile); + } + + $pipeline->next()($payload, $pipeline); + } +} diff --git a/src/HandlingDomain/Video/src/Pipeline/Upload/Step/SaveEntityStep.php b/src/HandlingDomain/Video/src/Pipeline/Upload/Step/SaveEntityStep.php new file mode 100644 index 0000000..d87820c --- /dev/null +++ b/src/HandlingDomain/Video/src/Pipeline/Upload/Step/SaveEntityStep.php @@ -0,0 +1,31 @@ +getVideo(); + + $this->entityManager->persist($video); + $this->entityManager->flush(); + } +} diff --git a/src/HandlingDomain/Video/src/Pipeline/Upload/Step/UploadFileStep.php b/src/HandlingDomain/Video/src/Pipeline/Upload/Step/UploadFileStep.php new file mode 100644 index 0000000..cbcfdd3 --- /dev/null +++ b/src/HandlingDomain/Video/src/Pipeline/Upload/Step/UploadFileStep.php @@ -0,0 +1,42 @@ +getVideo(); + $uploadedFile = $uploadPayload->getUploadedFile(); + + $directoryPath = $this->uploader->upload($uploadedFile, $video->getId()); + $video->setDirectoryPath($directoryPath); + + $this->imageBuilder->build($video->getId()); + + $pipeline->next()($payload, $pipeline); + } +} diff --git a/src/HandlingDomain/Video/src/Pipeline/Upload/UploadPayload.php b/src/HandlingDomain/Video/src/Pipeline/Upload/UploadPayload.php new file mode 100644 index 0000000..c0e86e7 --- /dev/null +++ b/src/HandlingDomain/Video/src/Pipeline/Upload/UploadPayload.php @@ -0,0 +1,32 @@ +uploadedFile; + } + public function setUploadedFile(UploadedFileInterface $uploadedFile): void + { + $this->uploadedFile = $uploadedFile; + } + + public function getVideo(): Video + { + return $this->video; + } + public function setVideo(Video $video): void + { + $this->video = $video; + } +} diff --git a/src/HandlingDomain/Video/src/Pipeline/Upload/UploadPipeline.php b/src/HandlingDomain/Video/src/Pipeline/Upload/UploadPipeline.php new file mode 100644 index 0000000..f83db80 --- /dev/null +++ b/src/HandlingDomain/Video/src/Pipeline/Upload/UploadPipeline.php @@ -0,0 +1,42 @@ +checkVideoStep, + $this->buildVideoStep, + $this->analyzeTitleStep, + $this->uploadFileStep, + $this->saveEntityStep, + ]); + } + + public function reset(): void + { + $this->tasks = [ + $this->checkVideoStep, + $this->buildVideoStep, + $this->analyzeTitleStep, + $this->uploadFileStep, + $this->saveEntityStep, + ]; + } +} diff --git a/src/HandlingDomain/Video/src/Rule/VideoExistsRule.php b/src/HandlingDomain/Video/src/Rule/VideoExistsRule.php new file mode 100644 index 0000000..e4d8874 --- /dev/null +++ b/src/HandlingDomain/Video/src/Rule/VideoExistsRule.php @@ -0,0 +1,29 @@ +tagRepository->findOneBy(['title' => $uploadedFile->getClientFilename()]) !== null; + } +} diff --git a/src/HandlingDomain/Video/src/Uploader/VideoUploader.php b/src/HandlingDomain/Video/src/Uploader/VideoUploader.php index a7b55c6..4829fdf 100644 --- a/src/HandlingDomain/Video/src/Uploader/VideoUploader.php +++ b/src/HandlingDomain/Video/src/Uploader/VideoUploader.php @@ -4,37 +4,31 @@ namespace MyTube\Handling\Video\Uploader; use Exception; use MyTube\Data\Business\Entity\Video; -use MyTube\Handling\Video\Builder\VideoImageBuilder; use Psr\Http\Message\UploadedFileInterface; +use Ramsey\Uuid\UuidInterface; class VideoUploader { - public function __construct( - private readonly VideoImageBuilder $imageBuilder, - ) { - } - public function upload( UploadedFileInterface $file, - Video $video, - ): void { - $targetPath = sprintf( + UuidInterface $directoryId, + ): string { + $directoryPath = sprintf( '%s/%s/%s/', APP_ROOT, 'var/filestore', - $video->getId()->toString(), + $directoryId->toString(), ); - if (file_exists($targetPath)) { + if (file_exists($directoryPath)) { throw new Exception('File already exists'); } - mkdir($targetPath, 0777, true); - $video->setDirectoryPath($targetPath); + mkdir($directoryPath, 0777, true); - $targetPath = $targetPath . 'video.' . substr(strrchr($file->getClientFilename(),'.'),1); + $targetPath = $directoryPath . 'video.' . substr(strrchr($file->getClientFilename(),'.'),1); $file->moveTo($targetPath); - $this->imageBuilder->build($video); + return $directoryPath; } } diff --git a/src/HandlingDomain/VideoList/config/service_manager.php b/src/HandlingDomain/VideoList/config/service_manager.php index 414fba4..7236630 100644 --- a/src/HandlingDomain/VideoList/config/service_manager.php +++ b/src/HandlingDomain/VideoList/config/service_manager.php @@ -1,10 +1,7 @@ [ /// CQRS // Read List - ReadListQueryHandler::class => InjectionFactory::class, ReadListQueryBuilder::class => AutoWiringFactory::class, + ReadListQueryHandler::class => InjectionFactory::class, + // Upload + UploadCommandBuilder::class => AutoWiringFactory::class, + UploadCommandHandler::class => AutoWiringFactory::class, ], ]; diff --git a/src/HandlingDomain/VideoList/src/Handler/Command/Upload/UploadCommand.php b/src/HandlingDomain/VideoList/src/Handler/Command/Upload/UploadCommand.php new file mode 100644 index 0000000..444e9e3 --- /dev/null +++ b/src/HandlingDomain/VideoList/src/Handler/Command/Upload/UploadCommand.php @@ -0,0 +1,18 @@ +uploadedFiles; + } +} diff --git a/src/HandlingDomain/VideoList/src/Handler/Command/Upload/UploadCommandBuilder.php b/src/HandlingDomain/VideoList/src/Handler/Command/Upload/UploadCommandBuilder.php new file mode 100644 index 0000000..de072c3 --- /dev/null +++ b/src/HandlingDomain/VideoList/src/Handler/Command/Upload/UploadCommandBuilder.php @@ -0,0 +1,16 @@ +getUploadedFiles() as $uploadedFile) { + try { + $payload = new UploadPayload(); + $payload->setUploadedFile($uploadedFile); + + $this->pipeline->reset(); + $this->pipeline->handle($payload); + + $uploadedFiles[] = UploadedFileResult::fromVideo($uploadedFile, $payload->getVideo()); + } catch (Throwable $e) { + $uploadedFiles[] = UploadedFileResult::fromException($uploadedFile, $e); + $this->logger->exception($e); + $this->logger->info($e->getTraceAsString()); + } + } + + return new UploadCommandResult($uploadedFiles); + } +} diff --git a/src/HandlingDomain/VideoList/src/Handler/Command/Upload/UploadCommandResult.php b/src/HandlingDomain/VideoList/src/Handler/Command/Upload/UploadCommandResult.php new file mode 100644 index 0000000..e0c5b66 --- /dev/null +++ b/src/HandlingDomain/VideoList/src/Handler/Command/Upload/UploadCommandResult.php @@ -0,0 +1,21 @@ +uploadedFileResults; + } +} diff --git a/src/HandlingDomain/VideoList/src/Model/UploadedFileResult.php b/src/HandlingDomain/VideoList/src/Model/UploadedFileResult.php new file mode 100644 index 0000000..77a95f3 --- /dev/null +++ b/src/HandlingDomain/VideoList/src/Model/UploadedFileResult.php @@ -0,0 +1,66 @@ +success = $this->video != null; + } + + public function getUploadedFile(): UploadedFileInterface + { + return $this->uploadedFile; + } + + public function getSuccess(): bool + { + return $this->success; + } + + public function getVideo(): ?Video + { + return $this->video; + } + + public function getException(): ?Throwable + { + return $this->exception; + } +} \ No newline at end of file diff --git a/src/Infrastructure/Exception/src/ErrorDomain.php b/src/Infrastructure/Exception/src/ErrorDomain.php index ac45722..bacf95e 100644 --- a/src/Infrastructure/Exception/src/ErrorDomain.php +++ b/src/Infrastructure/Exception/src/ErrorDomain.php @@ -11,4 +11,6 @@ enum ErrorDomain : string { case Registration = 'Registration'; case Product = 'Product'; case Video = 'Video'; + case Tag = 'Tag'; + case TagAlias = 'TagAlias'; } \ No newline at end of file