I saw the blog post about the updated Tempest View, and the view component code made me want to wash my eyes.
For years we are keeping logic out of templates, and in this templating engine logic is needed because it removed the possibility to make a ViewComponent
class.
Adding more separation
The framework has a Mapper class. And in the cache file I saw that the attributes are available as an array.
So the first thing I did is to make a data class that holds all the variables that are needed in the component.
use Tempest\Http\Session\Session;
use Tempest\Mapper\Strict;
use function Tempest\Support\str;
class InputMap
{
#[Strict]
public string $name {
set(string $value) {
$this->label = $value;
$this->id = $value;
$this->name = $value;
if($value != '') {
$this->errors = $this->session->getErrorsFor($value);
$this->previousValue = $this->session->getOriginalValueFor($value, $this->defaultValue);
}
}
}
public string $label {
set(string $value) => str($value)->title();
}
public string $type = 'text';
public string $id;
public array $errors = [];
public string $defaultValue = '';
public string $previousValue = '';
public function __construct(private Session $session)
{}
}
One bug the original code has it that the session methods throw an error if there is no name value given. The value is not enforced, so the error is a a needle in a haystack.
The framework comes with default Mapper
options, but the ArrayToObjectMapper
removes the id
property because most developers, like me, are too lazy to type the same value as the name
property.
So I had to create a custom Mapper
class.
use Tempest\Http\Session\Session;
use Tempest\Mapper\Exceptions\MappingValuesWereMissing;
use Tempest\Mapper\Mapper;
use Tempest\Mapper\Strict;
class InputMapper implements Mapper
{
public function __construct(private Session $session)
{}
public function canMap(mixed $from, mixed $to): bool
{
return is_a($to, InputMap::class, allow_string: true);
}
public function map(mixed $from, mixed $to): mixed
{
$inputMap = new InputMap($this->session);
$reflection = new \ReflectionClass($to);
$properties = $reflection->getProperties();
$missingProperties = [];
// catch the required properties.
foreach ($properties as $property) {
if($property->getAttributes(Strict::class) && in_array($property->getName(), array_keys($from->toArray())) == false) {
$missingProperties[] = $property->getName();
}
}
if(count($missingProperties) > 0) {
throw new MappingValuesWereMissing($to, $missingProperties);
}
// When there is no previousValue the defaultValue is displayed.
if(isset($from['defaultValue'])){
$inputMap->defaultValue = $from['defaultValue'];
unset($from['defaultValue']);
}
foreach($from as $name => $value) {
$inputMap->$name = $value;
}
return $inputMap;
}
}
This class does nothing fancy, the main function is that it is the hook for the Mapper
class.
With the MappingValuesWereMissing
exception displays at least the name of the missing variable, it makes the search less cumbersome.
The view component now is a lot shorter.
use App\Common\Components\InputMap;
use function Tempest\map;
$data = map($attributes)->to(InputMap::class);
?>
<div>
<label :for="$data->id">{{ $data->label }}</label>
<textarea :if="$data->type === 'textarea'" :name="$data->name" :id="$data->id">{{ $data->previousValue }}</textarea>
<input :else :type="$data->type" :name="$data->name" :id="$data->id" :value="$data->previousValue"/>
<div :if="$data->errors !== []">
<div :foreach="$data->errors as $error">
{{ $error->message() }}
</div>
</div>
</div>
I don't see the benefit for all those separate variables if you can make a data object.
Conclusion
With the map
function I was able to remove getting the Session
instance form the view component. And also all the logic that is related to the name
value.
If performance is the main focus the current framework code is the way to go.
My preferred way is adding a dataobject that needs no manipulation in the template engine. This isolates the value preprocessing from the rendering.
The code in the template is then just the markup.
Top comments (0)