Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
93
app/Models/Concerns/RecordsSlugChanges.php
Normal file
93
app/Models/Concerns/RecordsSlugChanges.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use App\Models\Redirect;
|
||||
use App\Support\SiteUrls;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Leaves a redirect behind when a slug changes.
|
||||
*
|
||||
* Renaming a slug breaks every link anybody shared, every bookmark and every
|
||||
* search result — silently, because nothing notices and the frontend only
|
||||
* finds out when a visitor gets a 404. The moment the slug changes is the only
|
||||
* moment the old address is still known, so that is when the note is written.
|
||||
*/
|
||||
trait RecordsSlugChanges
|
||||
{
|
||||
/**
|
||||
* The `site.paths` key this model publishes under.
|
||||
*/
|
||||
abstract public function publicPathType(): string;
|
||||
|
||||
public static function bootRecordsSlugChanges(): void
|
||||
{
|
||||
static::updated(function (Model $model): void {
|
||||
if (! $model->wasChanged('slug')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$model->recordSlugChange();
|
||||
});
|
||||
}
|
||||
|
||||
protected function recordSlugChange(): void
|
||||
{
|
||||
$type = $this->publicPathType();
|
||||
$previous = (array) json_decode((string) json_encode($this->getOriginal('slug')), true);
|
||||
$current = (array) $this->getTranslations('slug');
|
||||
|
||||
foreach (array_keys((array) config('locales.supported')) as $locale) {
|
||||
$was = (string) ($previous[$locale] ?? '');
|
||||
$now = (string) ($current[$locale] ?? '');
|
||||
|
||||
// Nothing to redirect from when the record had no address in this
|
||||
// language, and nowhere to redirect to when it no longer has one.
|
||||
if (blank($was) || blank($now) || $was === $now) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$from = Redirect::normalise(SiteUrls::path($type, $locale, $was));
|
||||
$to = Redirect::normalise(SiteUrls::path($type, $locale, $now));
|
||||
|
||||
if ($from === $to) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->writeRedirect($from, $to, $locale);
|
||||
}
|
||||
}
|
||||
|
||||
protected function writeRedirect(string $from, string $to, string $locale): void
|
||||
{
|
||||
$scope = fn () => Redirect::query()->where('locale', $locale);
|
||||
|
||||
// The new address resolves to this record again, so any rule pointing
|
||||
// away from it is now wrong — this is what renaming something back to
|
||||
// its original slug looks like.
|
||||
$scope()->where('from', $to)->delete();
|
||||
|
||||
// Whoever pointed at the old address now points at the new one, so a
|
||||
// frontend never has to follow two hops.
|
||||
$scope()->where('to', $from)->update(['to' => $to]);
|
||||
|
||||
$redirect = Redirect::query()->firstOrNew(['from' => $from, 'locale' => $locale]);
|
||||
|
||||
// `source` is set here rather than through mass assignment: it records
|
||||
// what the application did, not anything somebody submitted.
|
||||
$redirect->forceFill([
|
||||
'to' => $to,
|
||||
'status' => 301,
|
||||
'is_enabled' => true,
|
||||
'source_type' => $this->getMorphClass(),
|
||||
'source_id' => $this->getKey(),
|
||||
])->save();
|
||||
|
||||
// Belt and braces: a rule pointing at its own address would send a
|
||||
// browser round for ever.
|
||||
Redirect::query()->whereColumn('from', 'to')->delete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user