From f73e5a1af5473a84035e014dae31547dcb11b06c Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Wed, 6 May 2026 22:28:45 +0300 Subject: [PATCH] extend Post entity with feature slot Adds nullable feature_slot column (unique) plus repo findByFeatureSlot/findFeatured/update methods so admins can pin a post into one of two slots. --- backend/app/Post/EloquentPostRepository.php | 49 +++++++++++++++++ backend/app/Post/Post.php | 11 ++++ backend/app/Post/PostModel.php | 13 +++-- backend/app/Post/PostRepository.php | 14 +++++ ...05_06_000005_add_feature_slot_to_posts.php | 24 +++++++++ backend/tests/Fakes/FakePostRepository.php | 53 ++++++++++++++++++- 6 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 backend/database/migrations/2026_05_06_000005_add_feature_slot_to_posts.php diff --git a/backend/app/Post/EloquentPostRepository.php b/backend/app/Post/EloquentPostRepository.php index b8c90b5..074b90a 100644 --- a/backend/app/Post/EloquentPostRepository.php +++ b/backend/app/Post/EloquentPostRepository.php @@ -4,6 +4,7 @@ namespace App\Post; use DateTimeImmutable; use DateTimeZone; +use RuntimeException; class EloquentPostRepository implements PostRepository { @@ -65,6 +66,53 @@ class EloquentPostRepository implements PostRepository PostModel::query()->where('id', $id)->delete(); } + /** + * @throws RuntimeException + */ + public function update(Post $post): Post + { + $model = PostModel::find($post->getId()); + if ($model === null) { + throw new RuntimeException( + "Post with id: {$post->getId()} does not exist" + ); + } + $model->user_id = $post->getUserId(); + $model->title = $post->getTitle(); + $model->body = $post->getBody(); + $model->created_at = $post->getCreatedAt(); + $model->feature_slot = $post->getFeatureSlot(); + $model->save(); + + return $this->toDomain($model); + } + + public function findByFeatureSlot(int $slot): ?Post + { + $model = PostModel::query() + ->where('feature_slot', $slot) + ->first(); + + return $model === null ? null : $this->toDomain($model); + } + + /** + * @return Post[] + */ + public function findFeatured(): array + { + $models = PostModel::query() + ->whereNotNull('feature_slot') + ->orderBy('feature_slot', 'asc') + ->get(); + + return $models->map( + function (PostModel $model) { + return $this->toDomain($model); + }, + )->all(); + } + private function toDomain(PostModel $model): Post { $utc = new DateTimeZone('UTC'); @@ -78,6 +126,7 @@ class EloquentPostRepository implements PostRepository $model->created_at->toDateTimeString(), $utc, ), + featureSlot: $model->feature_slot, ); } } diff --git a/backend/app/Post/Post.php b/backend/app/Post/Post.php index 76d100e..2445318 100644 --- a/backend/app/Post/Post.php +++ b/backend/app/Post/Post.php @@ -12,6 +12,7 @@ class Post private string $title, private string $body, private DateTimeImmutable $createdAt, + private ?int $featureSlot, ) {} public function getId(): int @@ -38,4 +39,14 @@ class Post { return $this->createdAt; } + + public function getFeatureSlot(): ?int + { + return $this->featureSlot; + } + + public function isFeatured(): bool + { + return $this->featureSlot !== null; + } } diff --git a/backend/app/Post/PostModel.php b/backend/app/Post/PostModel.php index 357347b..6b566b6 100644 --- a/backend/app/Post/PostModel.php +++ b/backend/app/Post/PostModel.php @@ -2,22 +2,27 @@ namespace App\Post; +use DateTimeImmutable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Carbon; /** * @property int $id * @property int $user_id * @property string $title * @property string $body - * @property Carbon $created_at + * @property DateTimeImmutable $created_at + * @property ?int $feature_slot * * @method static Builder|PostModel newModelQuery() * @method static Builder|PostModel newQuery() * @method static Builder|PostModel query() * @method static Builder|PostModel whereId($value) * @method static Builder|PostModel whereUserId($value) + * @method static Builder|PostModel whereTitle($value) + * @method static Builder|PostModel whereBody($value) + * @method static Builder|PostModel whereCreatedAt($value) + * @method static Builder|PostModel whereFeatureSlot($value) * * @mixin \Eloquent */ @@ -32,9 +37,11 @@ class PostModel extends Model 'title', 'body', 'created_at', + 'feature_slot', ]; protected $casts = [ - 'created_at' => 'datetime', + 'created_at' => 'immutable_datetime', + 'feature_slot' => 'integer', ]; } diff --git a/backend/app/Post/PostRepository.php b/backend/app/Post/PostRepository.php index a00827b..1113949 100644 --- a/backend/app/Post/PostRepository.php +++ b/backend/app/Post/PostRepository.php @@ -2,6 +2,8 @@ namespace App\Post; +use RuntimeException; + interface PostRepository { public function create(CreatePostDto $dto): Post; @@ -19,4 +21,16 @@ interface PostRepository public function findRecent(int $limit): array; public function delete(int $id): void; + + /** + * @throws RuntimeException + */ + public function update(Post $post): Post; + + public function findByFeatureSlot(int $slot): ?Post; + + /** + * @return Post[] + */ + public function findFeatured(): array; } diff --git a/backend/database/migrations/2026_05_06_000005_add_feature_slot_to_posts.php b/backend/database/migrations/2026_05_06_000005_add_feature_slot_to_posts.php new file mode 100644 index 0000000..3531798 --- /dev/null +++ b/backend/database/migrations/2026_05_06_000005_add_feature_slot_to_posts.php @@ -0,0 +1,24 @@ +unsignedTinyInteger('feature_slot')->nullable(); + $table->unique('feature_slot'); + }); + } + + public function down(): void + { + Schema::table('posts', function (Blueprint $table) { + $table->dropUnique(['feature_slot']); + $table->dropColumn('feature_slot'); + }); + } +}; diff --git a/backend/tests/Fakes/FakePostRepository.php b/backend/tests/Fakes/FakePostRepository.php index ed64137..8f08fe1 100644 --- a/backend/tests/Fakes/FakePostRepository.php +++ b/backend/tests/Fakes/FakePostRepository.php @@ -5,6 +5,7 @@ namespace Tests\Fakes; use App\Post\CreatePostDto; use App\Post\Post; use App\Post\PostRepository; +use RuntimeException; class FakePostRepository implements PostRepository { @@ -22,10 +23,11 @@ class FakePostRepository implements PostRepository title: $dto->title, body: $dto->body, createdAt: $dto->createdAt, + featureSlot: null, ); $this->existingPosts[$id] = $post; - return $post; + return $this->copy($post); } public function find(int $id): ?Post @@ -85,6 +87,54 @@ class FakePostRepository implements PostRepository unset($this->existingPosts[$id]); } + /** + * @throws RuntimeException + */ + public function update(Post $post): Post + { + $id = $post->getId(); + if (! isset($this->existingPosts[$id])) { + throw new RuntimeException( + "Post with id: $id does not exist" + ); + } + $this->existingPosts[$id] = $post; + + return $this->copy($post); + } + + public function findByFeatureSlot(int $slot): ?Post + { + foreach ($this->existingPosts as $post) { + if ($post->getFeatureSlot() === $slot) { + return $this->copy($post); + } + } + + return null; + } + + /** + * @return Post[] + */ + public function findFeatured(): array + { + $featured = []; + foreach ($this->existingPosts as $post) { + if ($post->isFeatured()) { + $featured[] = $this->copy($post); + } + } + usort( + $featured, + function (Post $left, Post $right) { + return $left->getFeatureSlot() <=> $right->getFeatureSlot(); + }, + ); + + return $featured; + } + private function copy(Post $post): Post { return new Post( @@ -93,6 +143,7 @@ class FakePostRepository implements PostRepository title: $post->getTitle(), body: $post->getBody(), createdAt: $post->getCreatedAt(), + featureSlot: $post->getFeatureSlot(), ); }