Skip to content

Commit

Permalink
refactor: code cleanup
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas Frey <[email protected]>
  • Loading branch information
lukas-frey committed Oct 12, 2023
1 parent ad9b313 commit 8648281
Show file tree
Hide file tree
Showing 15 changed files with 445 additions and 211 deletions.
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ Filament Nested Resources allows you to create nested resources of any depth. Th

## Showcase

![Screenshot 1](docs/images/screenshot_01.png)
![Screenshot 2](docs/images/screenshot_02.png)

## Support us

Your support is key to the continual advancement of our plugin. We appreciate every user who has contributed to our journey so far.
Expand All @@ -24,6 +27,136 @@ composer require guava/filament-nested-resources
```

## Usage
Throughout the documentation I refer to `root` nested resource and `child` nested resources. The only difference is that the `Root` is the first resource in the relationship tree.

In the examples: ArtistResource > AlbumResource > SongResource

Artist would be the root resource, the other would be child resources.

### TL;DR
Replace all extends of `Resource`, `RelationManager`, `Page` classes with their corresponding `Nested` variants.

So instead of extending `Resource`, extend `NestedResource`, instead of extending `EditRecord`, extend `NestedEditRecord` and so on.

Where applicable (in child resources), implement `getAncestor` to define the parent of the nested resource.

Optionally remove `index` page for child nested resources.

### Detailed usage

In order to make to set up Nested Resources, you need to do these steps:

1. Extend `NestedResource` on your root resource and all child resources you want to nest under your root resource.
2. On your child `NestedResources`, implement the `getAncestor()` method and return an instance of the `Ancestor` config.
3. On your **child** `NestedResource` pages, extend the corresponding `Nested[Context]Record`. So instead of `EditRecord`, extend `NestedEditRecord` and so on.
4. Create a `RelationManager` to bind the Resources together. Extend `NestedRelationManager` instead of `RelationManager`.

Let's imagine the scenario from the Showcase screenshots, where we have this schema:
1. Artist Model.
2. Album Model (Belongs to Artist).
3. Song Model (Belongs to Album).

First we create `ArtistResource`:

```php
use Guava\Filament\NestedResources\Resources\NestedResource;

class ArtistResource extends NestedResource
{
//

public static function getRelations(): array
{
return [
AlbumsRelationManager::class,
];
}
}
```

Next, we create the `AlbumResource`:

```php
use Guava\Filament\NestedResources\Resources\NestedResource;
use Guava\Filament\NestedResources\Ancestor;

class AlbumResource extends NestedResource
{
//

public static function getRelations(): array
{
return [
// Repeat the same for Song Resource
];
}

public static function getAncestor() : ?Ancestor
{
// This is just a simple configuration with a few helper methods
return Ancestor::make(
ArtistResource::class, // Parent Resource Class
// Optionally you can pass a relationship name, if it's non-standard. The plugin will try to guess it otherwise
);
}
}
```

For each page for our `AlbumResource`, we extend the corresponding NestedPage:

```php
use Guava\Filament\NestedResources\Pages\NestedCreateRecord;

class CreateAlbum extends NestedCreateRecord
{
//
}
```

```php
use Guava\Filament\NestedResources\Pages\NestedEditRecord;

class EditAlbum extends NestedEditRecord
{
//
}
```

```php
use Guava\Filament\NestedResources\Pages\NestedListRecords;

class ListAlbums extends NestedListRecords
{
//
}
```

And finally we create the `AlbumsRelationManager`. We just need to extend the `NestedRelationManager` class here:

```php
use Guava\Filament\NestedResources\RelationManagers\NestedRelationManager;

class AlbumsRelationManager extends NestedRelationManager
{
//
}
```

Optionally, we recommend deleting the `index` page from your child `NestedResources` (in this case AlbumResource):

```php
public static function getPages(): array
{
return [
'create' => Pages\CreateAlbum::route('/create'),
'edit' => Pages\EditAlbum::route('/{record}/edit'),
];
}
```

The plugin will work either way, but the difference is that the breadcrumb URLs will redirect to different pages based on whether the index page exists or not.

If it exists, it will redirect to the index page (which might be a little confusing) and if it does NOT exist, it will redirect to the Parent Resource's Edit page. Since there's a relation manager, you will still have a list of all records.

## Contributing

Expand Down
Binary file modified docs/images/banner.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/screenshot_01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/screenshot_02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 77 additions & 0 deletions src/Ancestor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Guava\Filament\NestedResources;

use Illuminate\Database\Eloquent\Model;

class Ancestor
{
public function __construct(
protected string $resource,
protected ?string $relationship = null,
) {
}

public function getResource(): string
{
return $this->resource;
}

public function getRelationship(): string
{
if (! $this->relationship) {
return $this->getResource()::getModelLabel();
}

return $this->relationship;
}

public function getRouteParameterName(): string
{
return get_resource_route_parameter($this->getResource());
}

public function getRouteParameters(Model $record): array
{
$ancestor = $this;
$related = $record;

$parameters = [];
do {
// For 'create' actions, a model is not yet created, so the owner record is being sent.
// In this case, the ancestor relation and received model are offset by one level and this
// will re-sync the depth again.
if ($ancestor->getResource()::getModel() === $related::class) {
$parameters[$ancestor->getRouteParameterName()] = $related->id;
continue;
}

$related = $ancestor->getRelatedModel($related);
$parameters[$ancestor->getRouteParameterName()] = $related->id;
} while ($ancestor = $ancestor->getResource()::getAncestor());

return $parameters;
}

public function getNormalizedRouteParameters(Model $record): array
{
return array_values(
array_reverse(
$this->getRouteParameters($record)
)
);
}

public function getRelatedModel(Model $record): Model
{
return $record->{$this->getRelationship()};
}

public static function make(string $resource, string $relationship = null)
{
return app(static::class, [
'resource' => $resource,
'relationship' => $relationship,
]);
}
}
18 changes: 18 additions & 0 deletions src/Concerns/HasAncestor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Guava\Filament\NestedResources\Concerns;

use Guava\Filament\NestedResources\Ancestor;

trait HasAncestor
{
public static function hasAncestor(): bool
{
return static::getAncestor() !== null;
}

public static function getAncestor(): ?Ancestor
{
return null;
}
}
43 changes: 43 additions & 0 deletions src/Pages/NestedCreateRecord.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Guava\Filament\NestedResources\Pages;

use Filament\Resources\Pages\CreateRecord;
use Guava\Filament\NestedResources\Ancestor;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Arr;

class NestedCreateRecord extends CreateRecord
{
use NestedPage;

protected function mutateFormDataBeforeCreate(array $data): array
{
$ancestor = $this->getAncestorRecord();

return parent::mutateFormDataBeforeCreate([
...$data,
...$ancestor,
]);
}

protected function getAncestorRecord(): array
{
$id = Arr::last($this->getRouteParameterIds());
$ancestor = static::getResource()::getAncestor();
$record = $ancestor->getResource()::getModel()::find($id);
$fake = new (static::getModel())();
/** @var BelongsTo $relation */
$relation = $fake->{$ancestor->getRelationship()}();

return [$relation->getForeignKeyName() => $record->id];
}

protected function getRedirectUrlParameters(): array
{
/** @var Ancestor $ancestor */
$ancestor = $this::getResource()::getAncestor();

return $ancestor->getNormalizedRouteParameters($this->getRecord());
}
}
35 changes: 35 additions & 0 deletions src/Pages/NestedEditRecord.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Guava\Filament\NestedResources\Pages;

use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;

class NestedEditRecord extends EditRecord
{
use NestedPage;

protected function configureDeleteAction(DeleteAction $action): void
{
$resource = static::getResource();
$ancestor = $resource::getAncestor();

if (!$ancestor) {
parent::configureDeleteAction($action);
return;
}

$ancestorResource = $ancestor->getResource();

$action
->authorize($resource::canDelete($this->getRecord()))
->successRedirectUrl(
$resource::hasPage('index')
? $resource::getUrl('index')
: $ancestorResource::getUrl('edit', [
...$ancestor->getNormalizedRouteParameters($this->getRecord()),
])
)
;
}
}
30 changes: 30 additions & 0 deletions src/Pages/NestedListRecords.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Guava\Filament\NestedResources\Pages;

use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
use Filament\Tables;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Model;

class NestedListRecords extends ListRecords
{
use NestedPage;

protected function makeTable(): Table
{
return parent::makeTable()
->recordUrl(fn (Model $record) => static::getResource()::getUrl('edit', [
...static::getResource()::getAncestor()->getNormalizedRouteParameters($record),
'record' => $record,
]))
;
}

protected function configureCreateAction(Tables\Actions\CreateAction | CreateAction $action): void
{
parent::configureCreateAction($action);
$action->url(static::getResource()::getUrl('create', $this->getRouteParameterIds()));
}
}
Loading

0 comments on commit 8648281

Please sign in to comment.