About Sharp for Laravel — Part 5.
Commands are your new best friend.
Let’s pretend that when a Player is injured, we have to perform some task (notify someone, call an external service, anything). We could achieve this with a checkbox form field, in the Form itself, and a watcher in the update() form method (or even an Eloquent save() event listener), checking if the old value on some attribute is the same as the new one. But we won’t, because there is a much better way to approach this: to write a dedicated Command.
First, we have to write the actual Command class, which must extend Code16\Sharp\EntityList\Commands\InstanceCommand:
class SetPlayerInjuredCommand extends InstanceCommand
{
public function label(): string
{
return "Declare injured";
}
public function execute($instanceId, array $data = []): array
{
Player::findOrFail($instanceId)->setInjured(true);
return $this->reload();
}
}
As you can see, this class needs to implement two methods:
- execute(), which receives the related $instanceId (here, the Player’s id) and an optional $data array (more on that later);
- and label(), which is obviously the text of the link displayed to the user.
We next have to write the actual code to declare a Player injured. Let’s keep it simple:
class Player extends Model
{
[...] public function setInjured(bool $injured)
{
$this->update([
"injured" => $injured
]);
}
}
And the new migration:
class AddInjuredToPlayers extends Migration
{
public function up()
{
Schema::table('players', function (Blueprint $table) {
$table->boolean("injured")->default(false);
});
}
}
Like we’ve seen before on Filters, we have to declare our new Command in the Player’s entity list:
class PlayerSharpList extends SharpEntityList
{
[...]
function buildListConfig()
{
$this->setPaginated()
->setSearchable()
->addFilter("team", TeamFilter::class)
->addInstanceCommand(
"set_injured", SetPlayerInjuredCommand::class
);
} [...]
function getListData(EntityListQueryParams $params)
{
[...]
return $this
->setCustomTransformer("name", function($name,$player) {
return $player->name .
($player->injured ? " (<em>injured</em>)" : "");
})
[...]
}
}
The new transformer in getListData() takes care of the injury status display part, on the list.
One issue here is that the Player’s current status is not considered: we can declare an already injured Player injured, and worse there is no way to do the opposite.
The second part is easy, we simply write and declare another Command for recovery:
And for the first part of the problem, here’s how to allow (and therefore display) a Command based on the context:
class SetPlayerRecoveredCommand extends InstanceCommand
{
[...]
public function authorizeFor($instanceId): bool
{
return Player::findOrFail($instanceId)->injured;
}
}
(Of course, this authorizeFor() method can and should be also used to handle authorizations, and as we’ll see one day, Sharp can help on that too.)
What if we need to pass data to the Command?
Let’s say that we need to provide some details on the injury. Sharp’s solution to pass any data to a Command is… Forms. Here’s a simple example:
class SetPlayerInjuredCommand extends InstanceCommand
{
[...] public function execute($instanceId, array $data = []): array
{
$this->validate($data, [
"detail" => "required"
]);
Player::findOrFail($instanceId)->setInjured(true);
return $this->reload();
} [...] public function buildFormFields()
{
return $this->addField(
SharpFormTextareaField::make("detail")
->setLabel("Detail")
);
}
public function buildFormLayout(FormLayoutColumn &$column)
{
$column->withSingleField("detail");
}
}
By declaring buildFormFields() and buildFormLayout() methods, as we’ve done it for Entity Form (back in Part 1), we attach a form to the Command, wich can be as complex as needed—it’s the perfect place for an autocomplete on an external API. Regular validation is also possible, as shown in the execute() method here (of course, in real code, we should do something with this detail text…).
And here we go:
And what if the whole team is sick?
Well, this is quite a strange case, but there is a solution to that also: instead of and InstanceCommand, you can create an EntityCommand:
class SetAllTeamInjuredCommand extends EntityCommand
{
public function label(): string
{
return "Declare all team injured";
}
public function execute(EntityListQueryParams $params, array $data = []): array
{
Player::where("team_id", $params->filterFor("team"))
->get()
->each(function(Player $player) {
$player->setInjured(true);
});
return $this->reload();
}
public function confirmationText()
{
return "Are you like really sure?";
}
}
The only difference with InstanceCommand is on the execute() signature: instead of an $instanceId, we get a EntityListQueryParams $params. And since this Command is strange, we add a confirmationText() which Sharp will display before launching the Command.
Next we declare this new Command in the List:
class PlayerSharpList extends SharpEntityList
{
[...]
function buildListConfig()
{
$this->setPaginated()
->setSearchable()
->addFilter("team", TeamFilter::class)
->addInstanceCommand("set_injured", SetPlayerInjuredCommand::class)
->addInstanceCommand("set_recovered", SetPlayerRecoveredCommand::class)
->addEntityCommand("set_team_injured", SetAllTeamInjuredCommand::class);
} [...]
}
And we’re good to go:
Even if there is more to say on Commands, authorizations or return types for instance, I think this is a good place to call it a day. Next time we’ll talk maybe about a quite hard topic in content management: handling uploads, and pictures in particular [EDIT: part 6 is here].
As usual the source code of this part is available on Github, and to give Sharp a try, it’s all there.