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.
This commit is contained in:
Yisroel Baum 2026-05-06 22:28:45 +03:00
parent 64a334c63e
commit f73e5a1af5
Signed by: yisroelbaum
GPG key ID: 0FA60884F75520A9
6 changed files with 160 additions and 4 deletions

View file

@ -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,
);
}
}

View file

@ -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;
}
}

View file

@ -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<static>|PostModel newModelQuery()
* @method static Builder<static>|PostModel newQuery()
* @method static Builder<static>|PostModel query()
* @method static Builder<static>|PostModel whereId($value)
* @method static Builder<static>|PostModel whereUserId($value)
* @method static Builder<static>|PostModel whereTitle($value)
* @method static Builder<static>|PostModel whereBody($value)
* @method static Builder<static>|PostModel whereCreatedAt($value)
* @method static Builder<static>|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',
];
}

View file

@ -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;
}

View file

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->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');
});
}
};

View file

@ -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(),
);
}