Malcolm Turntbull
Malcolm Turntbull
FFilament
Created by Malcolm Turntbull on 11/14/2023 in #❓┊help
Does Filament change Laravel's default Route Model Binding?
My understanding of RMB from all the docs I've read is that the app() facade should be able to resolve the current Model, based on the URL parameters. This works within the scope of say, a Resource, but not within a custom class.
class TestSetup
{

protected function configureDto()
{
\Debugbar::alert(request()->getPathInfo());
$project = app(Project::class);
\Debugbar::alert($project);
// ---
}
}
class TestSetup
{

protected function configureDto()
{
\Debugbar::alert(request()->getPathInfo());
$project = app(Project::class);
\Debugbar::alert($project);
// ---
}
}
This logs the current route, with the correct ID in the correct path, e.g.
/company/2/projects/3
/company/2/projects/3
, however the $project value is a blank model - it is not resolving the ID in the path. Is this because the routes Filament gets us to create use {record} (e.g.
'view' => Pages\ViewProject::route('/{record}')
'view' => Pages\ViewProject::route('/{record}')
instead of {project}, thus rendering it unable to resolve? I've tried renaming the {record} to {project}, only to have it immediately throw a raft of errors. I'm really stuck with this one. By everything I've read, this stuff should just work, but it isn't. Is there some trick I'm missing? A trait my class needs to use? A setting on my Model? I don't know enough about how it works to know for sure, but it is beginning to feel like Filament is doing something tricky in the middle here with DI, and that trickiness is either undocumented / or I'm completely missing something obvious.
32 replies
FFilament
Created by Malcolm Turntbull on 11/9/2023 in #❓┊help
Performance tuning an enormous InfoList
Hi guys, Given the purely fictional example of an InfoList below...
class ProjectResource extends Resource
{
protected static ?string $model = Project::class;
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Section::make('overview')
->heading(fn(Project $project) => $project->name)
->schema([
TextEntry::make('Field 1')
->state(fn(Project $project) => ($project->isRunning()) ? $project->getStatusLabel() : $project->getCheckStatus()->getLabel())
->visible(fn(Project $project) => $project->isComplete()),

// ...

TextEntry::make('Field 100')
->state(fn(Project $project) => ($project->isRunning()) ?? $project->dependencies())
->visible(fn(Project $project) => $project->hasAnyOutput()),
])
]);
}
}
class ProjectResource extends Resource
{
protected static ?string $model = Project::class;
public static function infolist(Infolist $infolist): Infolist
{
return $infolist
->schema([
Section::make('overview')
->heading(fn(Project $project) => $project->name)
->schema([
TextEntry::make('Field 1')
->state(fn(Project $project) => ($project->isRunning()) ? $project->getStatusLabel() : $project->getCheckStatus()->getLabel())
->visible(fn(Project $project) => $project->isComplete()),

// ...

TextEntry::make('Field 100')
->state(fn(Project $project) => ($project->isRunning()) ?? $project->dependencies())
->visible(fn(Project $project) => $project->hasAnyOutput()),
])
]);
}
}
Where there are 100s of fields, and they all contain calculated content, with active rules around their visibility, so on and so on (picture a giant dashboard showing the monitoring and status of lots of different things), obviously this whole thing is going to be costly in terms of calculating its state, but I feel like it's being made MUCH worse by using Closures everywhere, and therefore instantiating brand new instances of objects each time. I've managed to work around some of this by hard caching results, but given the dynamics, I basically need to limit the lifetime to 5 seconds, and I'm writing a LOT of boilerplate to code to handle it.
3 replies
FFilament
Created by Malcolm Turntbull on 11/6/2023 in #❓┊help
Best Practice: Service Classes, Model methods or Filament Resources?
In the effort to keep my Models as slim as possible, I've been building up a stable of Service classes and dumping my business logic / factory methods in them. The problem is, I feel like I'm writing some smelly code, because I'm strongly binding my Services to instances of their associated Models, and so much of it feels like I'm just adding extra code for no real value - if a method only makes sense in the context of an instanced Model, then should I just bite the bullet and make it a method of the Model class? But in the midst of all this, I've also been thinking about Resources - in a sense, they're built in Service classes, and while they're not bound to a specific instance of a Model, maybe that's ok - if there's going to be a dumping ground of static methods that all accept Model instances as parameters, why not use the Resources we already have, instead of inventing a whole new layer of Service classes? I'm just wondering how other people tackle this overall concept. When you look at stock Laravel suggestions, it's always "use Services and Repositories!", but so far my attempts to follow suit in Filament don't really feel like they're solving any problems, just adding more code with no benefit. Any advice?
3 replies
FFilament
Created by Malcolm Turntbull on 10/18/2023 in #❓┊help
Exploring magical Model injection in FilamentPHP
Hi everyone, Everyhing is fine, but I wondered if there are any experts on Filament's DI I could quiz, because ever since learning about the magical DI available to us, I became completely consumed with the idea of never passing anything to a method ever again 😛 To help me understand a bit more about how the magic actually works though, here's an issue I'm running into: trying to get a Service class to DI the current instance of the Model. Take the following code as an example: In my EnvironmentResource, inside an Infolist...
Action::make('connect-repo')
->label('Connect your Repository')
->action(function (Environment $record, EnvironmentServiceInterface $environment_service) {
CreateGithubRepository::dispatch(new Collection([$record->fetchRepository()]));
});
Action::make('connect-repo')
->label('Connect your Repository')
->action(function (Environment $record, EnvironmentServiceInterface $environment_service) {
CreateGithubRepository::dispatch(new Collection([$record->fetchRepository()]));
});
and the Service...
class EnvironmentService implements EnvironmentServiceInterface
{
public function __construct(protected Environment $environment)
{

}
}
class EnvironmentService implements EnvironmentServiceInterface
{
public function __construct(protected Environment $environment)
{

}
}
and the ServiceProvider...
$this->app->bind(Environment\EnvironmentServiceInterface::class, Environment\EnvironmentService::class);
$this->app->bind(Environment\EnvironmentServiceInterface::class, Environment\EnvironmentService::class);
Now, as I said, everything is working fine, in the Action, DI is injecting the current Environment model just fine, and it's injecting the Service as well, BUT... the Environment model that the Service class attempts to inject, it's a blank model. Clearly the magic that is enabling the current instance of the Environment model to be injected (I presume it's Route Model Binding?) doesn't apply when a thing is calling a thing, rather than the thing being called directly. Obviously, I can work around this, and just pass in the Model as an argument, but... well, I was hoping someone could explain why it's not working in a little more detail, so I can both understand Laravel's DI better, but also so I can try and magic up a solution!
14 replies
FFilament
Created by Malcolm Turntbull on 10/12/2023 in #❓┊help
Adding unique names to Actions within a RepeatableEntry
Hi Everyone, Here's a trimmed down version of the code I'm trying:
Section::make('components')
->columns(8)
->heading('Environments')
->schema([
RepeatableEntry::make('environments')
->label("")
->schema([
Section::make('environment')
->heading(fn (Environment $record): string => $record->domain)
->label("")
->columns(8)
->schema([Actions::make([
Action::make('create-repo')
->label("Create new Repo")
->visible(fn (Environment $record): bool => $record->repository()->doesntExist())
->form(fn (Form $form): Form => RepositoryResource::form($form) )
->size(ActionSize::ExtraSmall)
->action(function (array $data, Environment $record) {
return $record->repository()->create($data);
}),
])
])
])
]);
Section::make('components')
->columns(8)
->heading('Environments')
->schema([
RepeatableEntry::make('environments')
->label("")
->schema([
Section::make('environment')
->heading(fn (Environment $record): string => $record->domain)
->label("")
->columns(8)
->schema([Actions::make([
Action::make('create-repo')
->label("Create new Repo")
->visible(fn (Environment $record): bool => $record->repository()->doesntExist())
->form(fn (Form $form): Form => RepositoryResource::form($form) )
->size(ActionSize::ExtraSmall)
->action(function (array $data, Environment $record) {
return $record->repository()->create($data);
}),
])
])
])
]);
The issue arises with having multiple action buttons all named "create-repo". Here are the things I've tried: * Adding a closure to make() - the method does not accept closures. * Creating some kind of random string - the buttons stop working completely, I think because every time it loads the button, it's getting a different name for it. * Accessing some version of $infolist or $record - they are not in scope unless injected into a closure. So basically, I'm stuck. Action buttons need a unique name, but I can't for the life of me figure out how to give them one. Am I pushing InfoLists WAY past what they are supposed to do, and should I be making a custom page / dashboard full of widgets?
6 replies
FFilament
Created by Malcolm Turntbull on 10/12/2023 in #❓┊help
Is it possible to add an Action to an Infolist Section header?
I've been trying something like the following, but it doesn't seem to display:
return $infolist
->schema([
Section::make('overview')
->action(
Action::make('add-new-project')
->form(fn (Form $form): Form => ProjectResource::form($form) )
->size(ActionSize::ExtraSmall)
)
->columns(8)
->heading('Project Overview')
->schema([
//
])
]);
return $infolist
->schema([
Section::make('overview')
->action(
Action::make('add-new-project')
->form(fn (Form $form): Form => ProjectResource::form($form) )
->size(ActionSize::ExtraSmall)
)
->columns(8)
->heading('Project Overview')
->schema([
//
])
]);
I can get Actions to display everywhere else, but they don't seem to want to sit in the header strip, right where they'd be most useful! I've tried the Action method, the RegisterActions method...
5 replies
FFilament
Created by Malcolm Turntbull on 10/8/2023 in #❓┊help
Showing a success message after form validation and before submission
Hi guys, consider this field:
Forms\Components\TextInput::make('name')
->required()
->maxLength(255)
->helperText(new HtmlString("The name you choose here will be the name used to set up your repository, e.g. https://github.com/company/<strong>name</strong>.git."))
->default(function($state, $livewire) {
return RepositoryService::createDefaultRepositoryName($livewire->ownerRecord->name);
} )
->live()
->unique(ignoreRecord: true)
->rules([
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
if ($get('type') === 'github' && GithubService::create()->repoExists($value)) {
$fail("This repository already exists!");
}
}
])
->afterStateUpdated(function (Forms\Contracts\HasForms $livewire, Forms\Components\TextInput $component) {
$livewire->validateOnly($component->getStatePath());
})
Forms\Components\TextInput::make('name')
->required()
->maxLength(255)
->helperText(new HtmlString("The name you choose here will be the name used to set up your repository, e.g. https://github.com/company/<strong>name</strong>.git."))
->default(function($state, $livewire) {
return RepositoryService::createDefaultRepositoryName($livewire->ownerRecord->name);
} )
->live()
->unique(ignoreRecord: true)
->rules([
fn (Get $get): Closure => function (string $attribute, $value, Closure $fail) use ($get) {
if ($get('type') === 'github' && GithubService::create()->repoExists($value)) {
$fail("This repository already exists!");
}
}
])
->afterStateUpdated(function (Forms\Contracts\HasForms $livewire, Forms\Components\TextInput $component) {
$livewire->validateOnly($component->getStatePath());
})
This succesfully shows an error message if the repository already exists, but I'd also like to show a green message if the repo name is ok! Also, bonus points if someone knows how to add some sort of spinner to the form while it's off doing its validation...
5 replies
FFilament
Created by Malcolm Turntbull on 9/29/2023 in #❓┊help
Bashing my head against using morphTo in a RelationManager. HELP!
Hi everyone, I am writing an app to automate the setup and configuration of websites, and I am trying to make it technology agnostic, so I am creating a lot of base classes to represent the core concept (e.g Repository), and then classes that inherit from them to represent the actual technology in use, e.g. Repository_Github or Repository_Bitbucket To represent these relationships, I am using a morphTo on the Repository, and a morphOne on Repository_Github. Bar Eloquent offering real inheritance, this model seems ok for now. The problem comes when I try and use a RepositoryRelationManager on the parent Project page. I have been fighting against it for an entire week, the RM just DOES NOT WANT to create / edit / resolve / basically anything with a morphTo relationship without serious hacking to get it to work. This has actually been a really good introduction to Filament for me, because I've gotten so deep into the guts trying to get this stuff to work, I feel like I'm getting fairly adept at it, but I have also realised, if I am bashing my head against it this hard, I am clearly doing something fundamentally wrong. So, can anyone help? What should I be doing other than using morphTo / RMs? Have I just chosen the wrong architecture? I don't need this inheritance model, but I do want the flexibility for a user to e.g create a Repository from a single form, and have all sorts of different fields and underlying methods based on the type of repo involved. It just feels like the app is pushing me towards dumping it all in a single table, and that feels really bad to me.
5 replies