Skip to content

Commit

Permalink
Make Layouts source from the filesystem
Browse files Browse the repository at this point in the history
  • Loading branch information
Kurt Friars committed Nov 21, 2023
1 parent 520e519 commit 915b875
Show file tree
Hide file tree
Showing 14 changed files with 500 additions and 73 deletions.
7 changes: 5 additions & 2 deletions database/migrations/create_layouts_table.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@
public function up()
{
Schema::create('layouts', function (Blueprint $table) {
$table->id();
$table->string('identifier')->unique()->nullable();
$table->id()->primary();
$table->string('key')->unique();
$table->string('name')->unique()->nullable();
$table->string('layoutable')->nullable();
$table->string('type');
$table->timestamps();
});
}
Expand Down
174 changes: 174 additions & 0 deletions src/Commands/SyncLayouts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<?php

namespace Plank\Contentable\Commands;

use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\File;
use Plank\Contentable\Contracts\Layout;
use Plank\Contentable\Contracts\Layoutable;
use Plank\Contentable\Enums\LayoutType;
use SplFileInfo;

class SyncLayouts extends Command
{
protected $signature = 'contentable:sync';

protected $description = 'Ensure defined layout files exist as Layout Models.';

/**
* @var Collection<Layoutable>|null
*/
protected ?Collection $layoutables = null;

public function handle(): void
{
foreach ($this->globalKeys() as $key) {
$this->ensureGlobalLayoutExists($key);
}

foreach ($this->layoutableKeys() as $key) {
$this->ensureModelLayoutExists($key);
}
}

/**
* Get the Layout keys available to all Layoutables
*/
protected function globalKeys(): array
{
$layoutModel = static::layoutModel();

return array_map(function (SplFileInfo $layout) use ($layoutModel): string {
return $layout->getBasename($layoutModel::extension());
}, File::files($layoutModel::folder()));
}

/**
* Get the class specific Layout keys
*/
protected function layoutableKeys(): array
{
$layoutModel = static::layoutModel();

$keys = [];

foreach (File::directories($layoutModel::folder()) as $path) {
$keys = array_merge($keys, $this->keysForPath($path));
}

return $keys;
}

protected function keysForPath(string $path): array
{
$layoutModel = static::layoutModel();

$key = (string) str($path)->afterLast(DIRECTORY_SEPARATOR);

return array_map(function (SplFileInfo $layout) use ($layoutModel, $key) : string {
return str($key)
->append($layoutModel::separator())
->append($layout->getBasename($layoutModel::extension()));
}, File::files($path));
}

protected function ensureGlobalLayoutExists(string $key): void
{
$layoutModel = static::layoutModel();

$layout = $layoutModel::query()
->where($layoutModel::getLayoutKeyColumn(), $key)
->first();

if ($layout !== null) {
return;
}

$layoutModel::query()->create([
$layoutModel::getLayoutKeyColumn() => $key,
$layoutModel::getNameColumn() => $this->globalKeyToName($key),
$layoutModel::getTypeColumn() => LayoutType::Global,
]);
}

protected function ensureModelLayoutExists(string $key): void
{
$layoutModel = static::layoutModel();

$layout = $layoutModel::query()
->where($layoutModel::getLayoutKeyColumn(), $key)
->first();

if ($layout !== null) {
return;
}

[$layoutableKey, $layoutKey] = explode($layoutModel::separator(), $key);

$type = match (strtolower($layoutKey)) {
'index' => LayoutType::Index,
'show' => LayoutType::Show,
default => LayoutType::Custom,
};

$layoutModel::query()->create([
$layoutModel::getLayoutKeyColumn() => $key,
$layoutModel::getNameColumn() => $this->layoutableKeyToName($layoutableKey, $layoutKey, $type),
$layoutModel::getTypeColumn() => $type,
$layoutModel::getLayoutableColumn() => $layoutableKey,
]);
}

/**
* Create a layout name for the given key
*/
protected function globalKeyToName(string $key): string
{
$layoutModel = static::layoutModel();

return str($key)
->replace($layoutModel::separator(), '_')
->snake()
->replace('_', ' ')
->title();
}

/**
* Create a layout name for the given key
*
* @param class-string<Layoutable> $layoutable
* @param LayoutType $type
*/
protected function layoutableKeyToName(string $layoutableKey, string $layoutKey, LayoutType $type): string
{
$layoutModel = static::layoutModel();

$modelName = str($layoutableKey)
->singular()
->snake()
->replace('_', ' ')
->title();

$name = str($layoutKey)
->replace($layoutModel::separator(), '_')
->snake()
->replace('_', ' ')
->title();

return match($type) {
LayoutType::Index => "$modelName Index",
LayoutType::Show => "$modelName Details",
default => "$name $modelName",
};
}

/**
* @return class-string<Layout&Model>
*/
protected static function layoutModel(): string
{
return config()->get('contentable.layouts.model');
}
}
2 changes: 1 addition & 1 deletion src/Concerns/CanRender.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static function bootCanRender()

public function content(): MorphOne
{
$contentModel = config('contentable.model');
$contentModel = config('contentable.content.model');

return $this->morphOne($contentModel, 'renderable');
}
Expand Down
2 changes: 1 addition & 1 deletion src/Concerns/HasContent.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ trait HasContent
{
public function contents(): MorphMany
{
$contentModel = config('contentable.model');
$contentModel = config('contentable.content.model');

return $this->morphMany($contentModel, 'contentable');
}
Expand Down
152 changes: 130 additions & 22 deletions src/Concerns/HasLayouts.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,166 @@

namespace Plank\Contentable\Concerns;

use Illuminate\Contracts\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Plank\Contentable\Contracts\Layout as LayoutContract;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Plank\Contentable\Contracts\Layout;
use Plank\Contentable\Contracts\Layoutable;
use Plank\Contentable\Enums\LayoutType;
use Plank\Contentable\Enums\LayoutMode;
use Plank\Contentable\Exceptions\MissingLayoutException;

/**
* @mixin Model
* @mixin Layoutable
*/
trait HasLayouts
{
public function layout(): Layout
{
if ($this->relatedLayout === null) {
throw MissingLayoutException::show(static::class, $this->getKey());
}

return $this->relatedLayout;
}

public static function indexLayout(): Layout
{
$layoutModel = static::layoutModel();

$layout = $layoutModel::query()
->where($layoutModel::getLayoutKeyColumn(), static::indexLayoutKey())
->first();

if ($layout === null) {
throw MissingLayoutException::index(static::class);
}

return $layout;
}

public function layouts(): Collection
{
$layoutModel = static::layoutModel();

return $layoutModel::query()
->where($layoutModel::getLayoutableColumn(), static::layoutKey())
->when($this->globalLayouts(), function (Builder $query) use ($layoutModel){
$query->orWhere(function (Builder $query) use ($layoutModel) {
$query->where($layoutModel::getTypeColumn(), LayoutType::Global)
->whereNotIn($layoutModel::getLayoutKeyColumn(), $this->excludedLayouts());
});
})
->get()
->sortBy($layoutModel::getNameColumn());
}

/**
* {@inheritDoc}
* The default implementation ships as a parent relationship
*
* @return BelongsTo
*/
public function layout(): LayoutContract
public function relatedLayout()
{
/** @var class-string<LayoutContract&Model> $model */
/** @var class-string<Layout&Model> $layoutModel */
$layoutModel = config('contentable.layouts.model');

return $layoutModel::query()
->where($layoutModel::getLayoutKeyName(), $this->layoutKey())
->firstOrFail();
return $this->belongsTo(
$layoutModel,
$this->layoutForeignKey(),
$this->layoutOwnerKey(),
);
}

/**
* {@inheritDoc}
* The foreign key for the layout relationship
*/
public function layoutKey(): string
protected function layoutForeignKey(): string
{
return str(class_basename($this))
->plural()
->lower()
->append('.show');
return 'layout_id';
}

/**
* {@inheritDoc}
* The primary key on the layouts table
*/
public static function indexLayout(): LayoutContract
protected function layoutOwnerKey(): string
{
/** @var class-string<LayoutContract&Model> $layoutModel */
$layoutModel = config('contentable.layouts.model');
return 'id';
}

return $layoutModel::query()
->where($layoutModel::getLayoutKeyName(), static::indexLayoutKey())
->first();
/**
* The key of the layout files for this Layoutable
*/
public static function layoutKey(): string
{
$layoutModel = static::layoutModel();

$name = str(class_basename(static::class))
->plural()
->snake();

return match ($layoutModel::mode()) {
LayoutMode::Blade => $name->lower(),
default => $name->studly(),
};
}

/**
* {@inheritDoc}
* The default index layout key of the Model
*/
public static function indexLayoutKey(): string
{
return str(class_basename(static::class))->plural()->lower()->append('.index');
$layoutModel = static::layoutModel();

return str(static::layoutKey())
->append($layoutModel::separator())
->append(static::indexKey());
}

/**
* The key we will use for the default index layouts
*/
protected static function indexKey(): string
{
$layoutModel = static::layoutModel();

return match ($layoutModel::mode()) {
LayoutMode::Blade => 'index',
default => 'Index',
};
}

/**
* @return class-string<Layout&Model>
*/
protected static function layoutModel(): string
{
return config()->get('contentable.layouts.model');
}

/**
* Determine if the Layoutable uses global layouts
*/
protected function globalLayouts(): bool
{
if (property_exists($this, 'globalLayouts')) {
return $this->globalLayouts;
}

return true;
}

/**
* Exclude specific layouts by their keys
*/
protected function excludedLayouts(): array
{
if (property_exists($this, 'excludedLayouts')) {
return $this->excludedLayouts;
}

return [];
}
}
Loading

0 comments on commit 915b875

Please sign in to comment.