Esplorando la gestione delle tipizzazioni in PHP

Per gestire i parametri di tipo array in PHP, possiamo usare semplicemente il tipo array, oppure esplorare soluzioni come DTOs o la classe Collection di Laravel. Un’altra opzione è utilizzare oggetti di valore personalizzati, generics tramite phpdoc o proprietà tipizzate accompagnate da docblock per migliori suggerimenti IDE. Laravel non supporta la tipizzazione strutturale sugli array, quindi per fare validazioni più precise, possiamo usare metodi come Arr::get.

1. Tipizzare un «menu item» in un componente Blade

PHP oggi non ha tipi “shape” nativi (ogni chiave dell’array con un proprio tipo), ma possiamo:

  1. Accettare un array tipizzato via PHPDoc – perfetto se vuoi massima semplicità e compatibilità.

  2. Creare un piccolo DTO (Data‑Transfer‑Object) – ottimo se desideri un vero tipo, validazione centralizzata e autocomplete IDE.

Di seguito trovi entrambi gli approcci: scegli tu quale adottare.


 A) Soluzione rapida con array + PHPDoc shaped array

namespace App\View\Layout\Main;
 
use App\View\Puk\PukComponent;
 
/**
 * @property array{
 *     label: string,
 *     url: string,
 *     icon?: string,
 *     children?: array<string, array>  // ricorsivo
 * } $item
 */
class MenuItem extends PukComponent
{
    /** @var array<string, mixed> */
    public array $item;
 
    /**
     * @param array{
     *     label: string,
     *     url: string,
     *     icon?: string,
     *     children?: array<string, array>
     * } $item
     */
    public function __construct(array $item)
    {
        parent::__construct();          // richiama la logica di PukComponent
        $this->item = $item;            // l’IDE saprà le chiavi disponibili
    }
 
    public function render()
    {
        return view('layout.main.menu-item');
    }
}

Blade (resources/views/layout/main/menu-item.blade.php)

<li x-data="{ open: false }" class="relative">
    {{-- link principale --}}
    <a href="{{ $item['url'] }}"
       class="flex items-center gap-2 px-3 py-2 rounded-md hover:bg-indigo-600/20"
       @if(isset($item['children'])) @click.prevent="open = !open" @endif>
        {{-- icona dinamica (blade-icons) --}}
        <x-dynamic-component :component="$item['icon'] ?? 'heroicon-s-link'"
                             class="shrink-0 size-5 text-indigo-400"/>
        <span class="flex-1">{{ $item['label'] }}</span>
 
        {{-- caret per i figli --}}
        @isset($item['children'])
            <x-heroicon-s-chevron-down x-show="!open" class="size-4 text-indigo-300"/>
            <x-heroicon-s-chevron-up   x-show="open"  class="size-4 text-indigo-300"/>
        @endisset
    </a>
 
    {{-- sotto‑voci, ricorsivo --}}
    @isset($item['children'])
        <ul x-show="open" x-collapse class="mt-1 space-y-1 pl-4 border-l border-indigo-700/30">
            @foreach($item['children'] as $child)
                <x-layout::main.menu-item :item="$child"/>
            @endforeach
        </ul>
    @endisset
</li>

Pro: minimo sforzo.
Contro: nessuna garanzia forte a run‑time; se mancano chiavi ti accorgi solo all’esecuzione.


 B) Soluzione “robusta” con DTO (MenuItemData)

DTO: app/Data/MenuItemData.php

namespace App\Data;
 
use Illuminate\Support\Collection;
 
final readonly class MenuItemData
{
    /** @param Collection<int, self>|self[] $children */
    public function __construct(
        public string $label,
        public string $url,
        public ?string $icon = null,
        public array|Collection $children = [],
    ) {}
 
    /** Costruzione “ricorsiva” da array normale */
    public static function from(array $data): self
    {
        $children = collect($data['children'] ?? [])
            ->map(fn (array $child) => self::from($child));
 
        return new self(
            label: $data['label'],
            url:   $data['url'],
            icon:  $data['icon']   ?? null,
            children: $children,
        );
    }
}

Componente: MenuItem.php

use App\Data\MenuItemData;
use App\View\Puk\PukComponent;
 
class MenuItem extends PukComponent
{
    public MenuItemData $item;
 
    public function __construct(MenuItemData|array $item)
    {
        parent::__construct();
 
        // consenti sia array che DTO
        $this->item = $item instanceof MenuItemData
            ? $item
            : MenuItemData::from($item);
    }
 
    public function render()
    {
        return view('layout.main.menu-item');
    }
}

Blade per il DTO

@php($hasChildren = $item->children->isNotEmpty())
<li x-data="{ open: false }" class="relative">
    <a href="{{ $item->url }}"
       class="flex items-center gap-2 px-3 py-2 rounded-md hover:bg-indigo-600/20"
       @if($hasChildren) @click.prevent="open = !open" @endif>
        <x-dynamic-component :component="$item->icon ?? 'heroicon-s-link'"
                             class="shrink-0 size-5 text-indigo-400"/>
        <span class="flex-1">{{ $item->label }}</span>
 
        @if($hasChildren)
            <x-heroicon-s-chevron-down x-show="!open" class="size-4 text-indigo-300"/>
            <x-heroicon-s-chevron-up   x-show="open"  class="size-4 text-indigo-300"/>
        @endif
    </a>
 
    @if($hasChildren)
        <ul x-show="open" x-collapse class="mt-1 space-y-1 pl-4 border-l border-indigo-700/30">
            @foreach($item->children as $child)
                <x-layout::main.menu-item :item="$child"/>
            @endforeach
        </ul>
    @endif
</li>

Pro: tipo forte, validazione in un solo punto, autocomplete preciso.
Contro: qualche riga di codice in più.


 ️Come si usa nel layout

{{-- $exampleMenu è l’array/DTO visto in precedenza --}}
@foreach($exampleMenu as $menuItem)
    <x-layout::main.menu-item :item="$menuItem"/>
@endforeach

Entrambe le versioni (array tipizzato o DTO) si integrano perfettamente con Alpine JS e Tailwind; scegli quella che si adatta meglio al tuo flusso di lavoro. In caso di dubbi o se vuoi aggiungere altre proprietà (badge, permessi, ecc.) fammi sapere!