diff --git a/public/app/edit.php b/public/app/edit.php new file mode 100644 index 0000000..0be7152 --- /dev/null +++ b/public/app/edit.php @@ -0,0 +1,25 @@ + new HtmlRenderer(), + 'json' => new JsonRenderer($normalizer), +]); +$format = $_GET['format'] ?? 'html'; + +$action = new EditAction($useCase, $renderContainer->get($format)); +$action((int) ($_GET['id'] ?? 0), $_GET['author'] ?? null, $_GET['title'] ?? null, $_GET['content'] ?? null, $format); diff --git a/public/app/list.php b/public/app/list.php new file mode 100644 index 0000000..97edd97 --- /dev/null +++ b/public/app/list.php @@ -0,0 +1,25 @@ + new HtmlRenderer(), + 'json' => new JsonRenderer($normalizer), +]); +$format = $_GET['format'] ?? 'html'; + +$action = new ListAction($useCase, $renderContainer->get($format)); +$action($format); diff --git a/public/app/new.php b/public/app/new.php new file mode 100644 index 0000000..cbca489 --- /dev/null +++ b/public/app/new.php @@ -0,0 +1,25 @@ + new HtmlRenderer(), + 'json' => new JsonRenderer($normalizer), +]); +$format = $_GET['format'] ?? 'html'; + +$action = new NewAction($useCase, $renderContainer->get($format)); +$action(random_int(0, 999999), $_GET['author'] ?? null, $_GET['title'] ?? null, $_GET['content'] ?? null, $format); diff --git a/public/app/show.php b/public/app/show.php new file mode 100644 index 0000000..b207c2d --- /dev/null +++ b/public/app/show.php @@ -0,0 +1,25 @@ + new HtmlRenderer(), + 'json' => new JsonRenderer($normalizer), +]); +$format = $_GET['format'] ?? 'html'; + +$action = new ShowAction($useCase, $renderContainer->get($format)); +$action((int) ($_GET['id'] ?? 0), $format); diff --git a/src/App/BlogPost/Application/UseCase/EditBlogPostUseCase.php b/src/App/BlogPost/Application/UseCase/EditBlogPostUseCase.php new file mode 100644 index 0000000..c12acab --- /dev/null +++ b/src/App/BlogPost/Application/UseCase/EditBlogPostUseCase.php @@ -0,0 +1,29 @@ +storage->get($id); + $blogPost->update( + $author ?? $this->rand->get('s'), + $title ?? $this->rand->get('m'), + $content ?? $this->rand->get('l') + ); + $this->storage->save($blogPost); + + return $blogPost; + } + +} diff --git a/src/App/BlogPost/Application/UseCase/FindBlogPostUseCase.php b/src/App/BlogPost/Application/UseCase/FindBlogPostUseCase.php new file mode 100644 index 0000000..ec48af6 --- /dev/null +++ b/src/App/BlogPost/Application/UseCase/FindBlogPostUseCase.php @@ -0,0 +1,18 @@ +storage->get($id); + } +} diff --git a/src/App/BlogPost/Application/UseCase/ListBlogPostUseCase.php b/src/App/BlogPost/Application/UseCase/ListBlogPostUseCase.php new file mode 100644 index 0000000..eb54184 --- /dev/null +++ b/src/App/BlogPost/Application/UseCase/ListBlogPostUseCase.php @@ -0,0 +1,17 @@ +storage->getAll(); + } +} diff --git a/src/App/BlogPost/Application/UseCase/NewBlogPostUseCase.php b/src/App/BlogPost/Application/UseCase/NewBlogPostUseCase.php new file mode 100644 index 0000000..61c142b --- /dev/null +++ b/src/App/BlogPost/Application/UseCase/NewBlogPostUseCase.php @@ -0,0 +1,28 @@ +rand->get('s'), + $title ?? $this->rand->get('m'), + $content ?? $this->rand->get('l'), + ); + $this->storage->save($blogPost); + + return $blogPost; + } +} diff --git a/src/App/BlogPost/Application/UseCase/RandomWords.php b/src/App/BlogPost/Application/UseCase/RandomWords.php new file mode 100644 index 0000000..6ff5660 --- /dev/null +++ b/src/App/BlogPost/Application/UseCase/RandomWords.php @@ -0,0 +1,30 @@ + 2, + 'm' => 3, + 'l' => 10, + }; + $string = ''; + for ($i = 0; $i < $times; ++$i) { + $string .= $this->randWord().' '; + } + + return trim($string); + } + + private function randWord(): string + { + $word = array_merge(range('a', 'z'), range('A', 'Z')); + shuffle($word); + $length = random_int(7, 12); + + return substr(implode($word), 0, $length); + } +} diff --git a/src/App/BlogPost/Domain/Model/BlogPost.php b/src/App/BlogPost/Domain/Model/BlogPost.php new file mode 100644 index 0000000..16498cf --- /dev/null +++ b/src/App/BlogPost/Domain/Model/BlogPost.php @@ -0,0 +1,62 @@ +createdAt = new DateTimeImmutable(); + } + + public function update( + ?string $author = null, + ?string $title = null, + ?string $content = null, + ): void { + $this->author = $author ?? $this->author; + $this->title = $title ?? $this->title; + $this->content = $content ?? $this->content; + $this->updatedAt = new DateTimeImmutable(); + } + + public function getId(): int + { + return $this->id; + } + + public function getAuthor(): string + { + return $this->author; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getContent(): string + { + return $this->content; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): ?DateTimeImmutable + { + return $this->updatedAt; + } +} + diff --git a/src/App/BlogPost/Domain/Model/BlogPostNormalizer.php b/src/App/BlogPost/Domain/Model/BlogPostNormalizer.php new file mode 100644 index 0000000..96a10a2 --- /dev/null +++ b/src/App/BlogPost/Domain/Model/BlogPostNormalizer.php @@ -0,0 +1,40 @@ + $blogPost->getId(), + 'author' => $blogPost->getAuthor(), + 'title' => $blogPost->getTitle(), + 'content' => $blogPost->getContent(), + 'created_at' => $blogPost->getCreatedAt()->format('Y-m-d H:i:s'), + 'updated_at' => $blogPost->getUpdatedAt()?->format('Y-m-d H:i:s'), + ]; + } + + public function denormalize(array $data): BlogPost + { + $blogPost = new BlogPost($data['id'], $data['author'], $data['title'], $data['content']); + $this->setTimestampProperties($blogPost, $data['created_at'], $data['updated_at']); + + return $blogPost; + } + + private function setTimestampProperties(BlogPost $blogPost, string $created, ?string $updated): void + { + $createdAt = new \ReflectionProperty($blogPost, 'createdAt'); + $createdAt->setAccessible(true); + $createdAt->setValue($blogPost, new DateTimeImmutable($created)); + if (null !== $updated) { + $updatedAt = new \ReflectionProperty($blogPost, 'updatedAt'); + $updatedAt->setAccessible(true); + $updatedAt->setValue($blogPost, new DateTimeImmutable($updated)); + } + } +} diff --git a/src/App/BlogPost/Domain/Model/Normalizer.php b/src/App/BlogPost/Domain/Model/Normalizer.php new file mode 100644 index 0000000..11000b2 --- /dev/null +++ b/src/App/BlogPost/Domain/Model/Normalizer.php @@ -0,0 +1,18 @@ + + */ + public function getAll(): iterable; + + public function save(BlogPost $blogPost): void; +} diff --git a/src/App/BlogPost/Infrastructure/Persistence/FileStorage.php b/src/App/BlogPost/Infrastructure/Persistence/FileStorage.php new file mode 100644 index 0000000..80994e2 --- /dev/null +++ b/src/App/BlogPost/Infrastructure/Persistence/FileStorage.php @@ -0,0 +1,62 @@ + + */ + private array $collection = []; + + public function __construct( + private readonly Normalizer $normalizer, + private readonly string $filePath, + ) { + if (!is_readable($filePath)) { + throw new InvalidArgumentException('Cannot read file '.$filePath); + } + $content = file_get_contents($filePath); + if (empty($content)) { + $content = serialize([]); + } + $this->collection = unserialize($content, ['allowed_classes' => false]); + } + + public function get(int $id): BlogPost + { + foreach ($this->collection as $item) { + if ($id === $item['id']) { + return $this->normalizer->denormalize($item); + } + } + throw new InvalidArgumentException('Cannot find blog post with id: '.$id); + } + + public function getAll(): iterable + { + return array_map([$this->normalizer, 'denormalize'], $this->collection); + } + + public function save(BlogPost $blogPost): void + { + $found = false; + foreach ($this->collection as $key => $item) { + if ($blogPost->getId() === $item['id']) { + $this->collection[$key] = $this->normalizer->normalize($blogPost); + $found = true; + break; + } + } + if (!$found) { + $this->collection[] = $this->normalizer->normalize($blogPost); + } + file_put_contents($this->filePath, serialize($this->collection)); + } +} diff --git a/src/App/BlogPost/Presentation/Controller/EditAction.php b/src/App/BlogPost/Presentation/Controller/EditAction.php new file mode 100644 index 0000000..97673a5 --- /dev/null +++ b/src/App/BlogPost/Presentation/Controller/EditAction.php @@ -0,0 +1,28 @@ +useCase->execute($id, $author, $title, $content); + + if ('json' === $format) { + header('Content-type: application/json'); + } else { + echo '

Blog post

'; + } + + echo $this->renderer->render($blogPost); + } +} diff --git a/src/App/BlogPost/Presentation/Controller/ListAction.php b/src/App/BlogPost/Presentation/Controller/ListAction.php new file mode 100644 index 0000000..d466302 --- /dev/null +++ b/src/App/BlogPost/Presentation/Controller/ListAction.php @@ -0,0 +1,40 @@ +useCase->execute(); + + if ('json' === $format) { + $count = count($blogPosts); + header('Content-type: application/json'); + echo '['; + foreach ($blogPosts as $num => $blogPost) { + echo $this->renderer->render($blogPost); + if ($num < $count - 1) { + echo ','; + } + } + echo ']'; + + return; + } + + echo '

Blog post list

'; + foreach ($blogPosts as $blogPost) { + echo $this->renderer->render($blogPost); + } + } +} diff --git a/src/App/BlogPost/Presentation/Controller/NewAction.php b/src/App/BlogPost/Presentation/Controller/NewAction.php new file mode 100644 index 0000000..72c1edd --- /dev/null +++ b/src/App/BlogPost/Presentation/Controller/NewAction.php @@ -0,0 +1,28 @@ +useCase->execute($id, $author, $title, $content); + + if ('json' === $format) { + header('Content-type: application/json'); + } else { + echo '

New blog post

'; + } + + echo $this->renderer->render($blogPost); + } +} diff --git a/src/App/BlogPost/Presentation/Controller/ShowAction.php b/src/App/BlogPost/Presentation/Controller/ShowAction.php new file mode 100644 index 0000000..7936b81 --- /dev/null +++ b/src/App/BlogPost/Presentation/Controller/ShowAction.php @@ -0,0 +1,28 @@ +useCase->execute($id); + + if ('json' === $format) { + header('Content-type: application/json'); + } else { + echo '

Blog post

'; + } + + echo $this->renderer->render($blogPost); + } +} diff --git a/src/App/BlogPost/Presentation/Renderer/HtmlRenderer.php b/src/App/BlogPost/Presentation/Renderer/HtmlRenderer.php new file mode 100644 index 0000000..e76a427 --- /dev/null +++ b/src/App/BlogPost/Presentation/Renderer/HtmlRenderer.php @@ -0,0 +1,42 @@ +getId(), + $blogPost->getAuthor(), + $blogPost->getTitle(), + $this->format($blogPost->getCreatedAt()), + $this->format($blogPost->getUpdatedAt()), + $blogPost->getContent(), + ]; + + return \vsprintf($this->getTemplate(), $placeholders); + } + + private function format(?DateTimeImmutable $date): string + { + return null === $date ? '' : $date->format('Y-m-d H:i:s'); + } + + private function getTemplate(): string + { + return <<<'HTML' +
+

ID %s

+

Author %s

+

Title %s

+

Created %s

+

Updated %s

+
%s
+
+ HTML; + } +} diff --git a/src/App/BlogPost/Presentation/Renderer/JsonRenderer.php b/src/App/BlogPost/Presentation/Renderer/JsonRenderer.php new file mode 100644 index 0000000..175d9af --- /dev/null +++ b/src/App/BlogPost/Presentation/Renderer/JsonRenderer.php @@ -0,0 +1,22 @@ +normalizer->normalize($blogPost), JSON_THROW_ON_ERROR); + } +} diff --git a/src/App/BlogPost/Presentation/Renderer/RenderContainer.php b/src/App/BlogPost/Presentation/Renderer/RenderContainer.php new file mode 100644 index 0000000..65d45d3 --- /dev/null +++ b/src/App/BlogPost/Presentation/Renderer/RenderContainer.php @@ -0,0 +1,24 @@ + $renderers + */ + public function __construct(private readonly array $renderers) + { + } + + public function get(string $format): Renderer + { + if (!isset($this->renderers[$format])) { + throw new InvalidArgumentException('Unsupported format: '.$format); + } + + return $this->renderers[$format]; + } +} diff --git a/src/App/BlogPost/Presentation/Renderer/Renderer.php b/src/App/BlogPost/Presentation/Renderer/Renderer.php new file mode 100644 index 0000000..040ebf6 --- /dev/null +++ b/src/App/BlogPost/Presentation/Renderer/Renderer.php @@ -0,0 +1,10 @@ +