Blog MoonShine 4 · Laravel 13

CRUD con MoonShine 4 y Laravel 13: tienda con 5 tablas y relaciones

La mayoría de tutoriales de MoonShine muestran un solo modelo. Este construye un sistema real con 5 tablas relacionadas — categorías, clientes, productos, pedidos y tabla pivot — usando la arquitectura de tres páginas por resource que introduce MoonShine 4.

01

Mapa del proyecto

Vamos a construir un sistema de tienda con estas cinco tablas y sus relaciones:

Category ──hasMany──► Product ◄──belongsToMany──► Order ──belongsTo──► Client

Tablas físicas:
  categories       → id, name, description
  clients          → id, name, email, phone
  products         → id, name, price, stock, category_id (FK)
  orders           → id, date, status, client_id (FK)
  order_product    → order_id, product_id, quantity  ← pivot

Al terminar el panel de MoonShine mostrará:

  1. Categorías con tabla de productos en la vista detalle (HasMany)
  2. Productos con dropdown de categoría en el formulario (BelongsTo)
  3. Clientes con tabla de pedidos en la vista detalle (HasMany)
  4. Pedidos con selector de cliente y selector múltiple de productos con campo cantidad (BelongsToMany + pivot)
02

Instalación

composer create-project laravel/laravel tienda
cd tienda
composer require moonshine/moonshine
php artisan moonshine:install

El instalador crea el usuario admin, publica el layout, el service provider y las rutas del panel. Configura tu base de datos en .env:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=tienda
DB_USERNAME=root
DB_PASSWORD=
03

Migraciones

Creá las migraciones respetando el orden de las FK — primero las tablas independientes, al final la pivot:

php artisan make:migration create_categories_table
php artisan make:migration create_clients_table
php artisan make:migration create_products_table
php artisan make:migration create_orders_table
php artisan make:migration create_order_product_table

categories

Schema::create('categories', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->text('description')->nullable();
    $table->timestamps();
});

clients

Schema::create('clients', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('email')->unique();
    $table->string('phone')->nullable();
    $table->timestamps();
});

products — FK a categories:

Schema::create('products', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->decimal('price', 10, 2);
    $table->integer('stock')->default(0);
    $table->foreignId('category_id')->constrained()->cascadeOnDelete();
    $table->timestamps();
});

orders — FK a clients, con campo enum para estados:

Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->date('date');
    $table->enum('status', ['pendiente','enviado','entregado','cancelado'])
          ->default('pendiente');
    $table->foreignId('client_id')->constrained()->cascadeOnDelete();
    $table->timestamps();
});

order_product — tabla pivot con campo extra quantity:

Schema::create('order_product', function (Blueprint $table) {
    $table->foreignId('order_id')->constrained()->cascadeOnDelete();
    $table->foreignId('product_id')->constrained()->cascadeOnDelete();
    $table->integer('quantity')->default(1);
    $table->primary(['order_id', 'product_id']);
});
php artisan migrate
04

Modelos y relaciones

Category

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    protected $fillable = ['name', 'description'];

    public function products()
    {
        return $this->hasMany(Product::class);
    }
}

Client

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Client extends Model
{
    protected $fillable = ['name', 'email', 'phone'];

    public function orders()
    {
        return $this->hasMany(Order::class);
    }
}

Product — pertenece a una categoría y puede estar en muchos pedidos:

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    protected $fillable = ['name', 'price', 'stock', 'category_id'];

    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    public function orders()
    {
        return $this->belongsToMany(Order::class)
                    ->withPivot('quantity')
                    ->withTimestamps();
    }
}

Order — pertenece a un cliente y tiene muchos productos con campo pivot:

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    protected $fillable = ['date', 'status', 'client_id'];
    protected $casts    = ['date' => 'date'];

    public function client()
    {
        return $this->belongsTo(Client::class);
    }

    public function products()
    {
        return $this->belongsToMany(Product::class)
                    ->withPivot('quantity')
                    ->withTimestamps();
    }
}
withPivot('quantity') es obligatorio en ambos lados de la relación. Sin él MoonShine no puede leer ni guardar el campo quantity de la tabla pivot.
05

Arquitectura de Resources en MoonShine 4

En MoonShine 4 cada resource genera 4 archivos organizados en subcarpetas — una diferencia clave respecto a MoonShine 3:

php artisan moonshine:resource CategoryResource
php artisan moonshine:resource ClientResource
php artisan moonshine:resource ProductResource
php artisan moonshine:resource OrderResource

Estructura generada para cada resource:

app/MoonShine/Resources/
└── Category/
    ├── CategoryResource.php        ← registra las 3 páginas
    └── Pages/
        ├── CategoryIndexPage.php   ← listado / tabla
        ├── CategoryFormPage.php    ← formulario crear/editar + validación
        └── CategoryDetailPage.php  ← vista detalle del registro

Cada página define sus propios fields(). El resource principal solo referencia las páginas.

06

Resource: Category

CategoryResource.php

<?php
namespace App\MoonShine\Resources\Category;

use App\Models\Category;
use App\MoonShine\Resources\Category\Pages\CategoryDetailPage;
use App\MoonShine\Resources\Category\Pages\CategoryFormPage;
use App\MoonShine\Resources\Category\Pages\CategoryIndexPage;
use MoonShine\Laravel\Resources\ModelResource;

class CategoryResource extends ModelResource
{
    protected string $model  = Category::class;
    protected string $title  = 'Categorías';
    protected string $column = 'name';

    protected function pages(): array
    {
        return [
            CategoryIndexPage::class,
            CategoryFormPage::class,
            CategoryDetailPage::class,
        ];
    }
}

CategoryIndexPage.php — columnas del listado:

<?php
namespace App\MoonShine\Resources\Category\Pages;

use MoonShine\Laravel\Pages\Crud\IndexPage;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Text;
use MoonShine\UI\Fields\Textarea;

class CategoryIndexPage extends IndexPage
{
    protected function fields(): iterable
    {
        return [
            ID::make()->sortable(),
            Text::make('Nombre', 'name'),
            Textarea::make('Descripción', 'description'),
        ];
    }
}

CategoryFormPage.php — formulario + validación:

<?php
namespace App\MoonShine\Resources\Category\Pages;

use MoonShine\Laravel\Pages\Crud\FormPage;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Text;
use MoonShine\UI\Fields\Textarea;

class CategoryFormPage extends FormPage
{
    protected function fields(): iterable
    {
        return [
            ID::make(),
            Text::make('Nombre', 'name')->required(),
            Textarea::make('Descripción', 'description'),
        ];
    }

    protected function rules(mixed $item): array
    {
        return [
            'name' => ['required', 'string', 'max:255'],
        ];
    }
}

CategoryDetailPage.php — detalle con tabla de productos relacionados (HasMany):

<?php
namespace App\MoonShine\Resources\Category\Pages;

use App\MoonShine\Resources\Product\ProductResource;
use MoonShine\Laravel\Fields\Relationships\HasMany;
use MoonShine\Laravel\Pages\Crud\DetailPage;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Text;
use MoonShine\UI\Fields\Textarea;

class CategoryDetailPage extends DetailPage
{
    protected function fields(): iterable
    {
        return [
            ID::make(),
            Text::make('Nombre', 'name'),
            Textarea::make('Descripción', 'description'),
            HasMany::make('Productos', 'products', resource: ProductResource::class),
        ];
    }
}
07

Resource: Client

ClientResource.php

<?php
namespace App\MoonShine\Resources\Client;

use App\Models\Client;
use App\MoonShine\Resources\Client\Pages\ClientDetailPage;
use App\MoonShine\Resources\Client\Pages\ClientFormPage;
use App\MoonShine\Resources\Client\Pages\ClientIndexPage;
use MoonShine\Laravel\Resources\ModelResource;

class ClientResource extends ModelResource
{
    protected string $model  = Client::class;
    protected string $title  = 'Clientes';
    protected string $column = 'name';

    protected function pages(): array
    {
        return [
            ClientIndexPage::class,
            ClientFormPage::class,
            ClientDetailPage::class,
        ];
    }
}

ClientIndexPage.php

<?php
namespace App\MoonShine\Resources\Client\Pages;

use MoonShine\Laravel\Pages\Crud\IndexPage;
use MoonShine\UI\Fields\Email;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Phone;
use MoonShine\UI\Fields\Text;

class ClientIndexPage extends IndexPage
{
    protected function fields(): iterable
    {
        return [
            ID::make()->sortable(),
            Text::make('Nombre', 'name'),
            Email::make('Correo', 'email'),
            Phone::make('Teléfono', 'phone'),
        ];
    }
}

ClientFormPage.php — validación de email único ignorando el propio registro:

<?php
namespace App\MoonShine\Resources\Client\Pages;

use MoonShine\Laravel\Pages\Crud\FormPage;
use MoonShine\UI\Fields\Email;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Phone;
use MoonShine\UI\Fields\Text;

class ClientFormPage extends FormPage
{
    protected function fields(): iterable
    {
        return [
            ID::make(),
            Text::make('Nombre', 'name')->required(),
            Email::make('Correo', 'email')->required(),
            Phone::make('Teléfono', 'phone'),
        ];
    }

    protected function rules(mixed $item): array
    {
        return [
            'name'  => ['required', 'string', 'max:255'],
            'email' => [
                'required', 'email',
                'unique:clients,email,' . ($item?->id ?? 'NULL'),
            ],
        ];
    }
}

ClientDetailPage.php — pedidos del cliente como HasMany:

<?php
namespace App\MoonShine\Resources\Client\Pages;

use App\MoonShine\Resources\Order\OrderResource;
use MoonShine\Laravel\Fields\Relationships\HasMany;
use MoonShine\Laravel\Pages\Crud\DetailPage;
use MoonShine\UI\Fields\Email;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Phone;
use MoonShine\UI\Fields\Text;

class ClientDetailPage extends DetailPage
{
    protected function fields(): iterable
    {
        return [
            ID::make(),
            Text::make('Nombre', 'name'),
            Email::make('Correo', 'email'),
            Phone::make('Teléfono', 'phone'),
            HasMany::make('Pedidos', 'orders', resource: OrderResource::class),
        ];
    }
}
08

Resource: Product

ProductResource.php — carga eager la categoría con $with:

<?php
namespace App\MoonShine\Resources\Product;

use App\Models\Product;
use App\MoonShine\Resources\Product\Pages\ProductDetailPage;
use App\MoonShine\Resources\Product\Pages\ProductFormPage;
use App\MoonShine\Resources\Product\Pages\ProductIndexPage;
use MoonShine\Laravel\Resources\ModelResource;

class ProductResource extends ModelResource
{
    protected string $model  = Product::class;
    protected string $title  = 'Productos';
    protected string $column = 'name';
    protected array  $with   = ['category'];

    protected function pages(): array
    {
        return [
            ProductIndexPage::class,
            ProductFormPage::class,
            ProductDetailPage::class,
        ];
    }
}

ProductIndexPage.php — muestra la categoría con BelongsTo:

<?php
namespace App\MoonShine\Resources\Product\Pages;

use App\MoonShine\Resources\Category\CategoryResource;
use MoonShine\Laravel\Fields\Relationships\BelongsTo;
use MoonShine\Laravel\Pages\Crud\IndexPage;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Number;
use MoonShine\UI\Fields\Text;

class ProductIndexPage extends IndexPage
{
    protected function fields(): iterable
    {
        return [
            ID::make()->sortable(),
            Text::make('Nombre', 'name'),
            Number::make('Precio', 'price'),
            Number::make('Stock', 'stock'),
            BelongsTo::make('Categoría', 'category', resource: CategoryResource::class),
        ];
    }
}

ProductFormPage.php — dropdown de categorías en el formulario:

<?php
namespace App\MoonShine\Resources\Product\Pages;

use App\MoonShine\Resources\Category\CategoryResource;
use MoonShine\Laravel\Fields\Relationships\BelongsTo;
use MoonShine\Laravel\Pages\Crud\FormPage;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Number;
use MoonShine\UI\Fields\Text;

class ProductFormPage extends FormPage
{
    protected function fields(): iterable
    {
        return [
            ID::make(),
            Text::make('Nombre', 'name')->required(),
            Number::make('Precio', 'price')->min(0)->required(),
            Number::make('Stock', 'stock')->min(0)->default(0),
            BelongsTo::make('Categoría', 'category', resource: CategoryResource::class)
                     ->required(),
        ];
    }

    protected function rules(mixed $item): array
    {
        return [
            'name'        => ['required', 'string', 'max:255'],
            'price'       => ['required', 'numeric', 'min:0'],
            'stock'       => ['integer', 'min:0'],
            'category_id' => ['required', 'exists:categories,id'],
        ];
    }
}
09

Resource: Order — BelongsToMany con pivot

Este es el resource más complejo y el que más preguntas genera. El formulario de pedidos necesita seleccionar múltiples productos y registrar una cantidad por cada uno en la tabla pivot.

OrderResource.php

<?php
namespace App\MoonShine\Resources\Order;

use App\Models\Order;
use App\MoonShine\Resources\Order\Pages\OrderDetailPage;
use App\MoonShine\Resources\Order\Pages\OrderFormPage;
use App\MoonShine\Resources\Order\Pages\OrderIndexPage;
use MoonShine\Laravel\Resources\ModelResource;

class OrderResource extends ModelResource
{
    protected string $model  = Order::class;
    protected string $title  = 'Pedidos';
    protected string $column = 'id';
    protected array  $with   = ['client', 'products'];

    protected function pages(): array
    {
        return [
            OrderIndexPage::class,
            OrderFormPage::class,
            OrderDetailPage::class,
        ];
    }
}

OrderIndexPage.php

<?php
namespace App\MoonShine\Resources\Order\Pages;

use App\MoonShine\Resources\Client\ClientResource;
use MoonShine\Laravel\Fields\Relationships\BelongsTo;
use MoonShine\Laravel\Pages\Crud\IndexPage;
use MoonShine\UI\Fields\Date;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Select;

class OrderIndexPage extends IndexPage
{
    protected function fields(): iterable
    {
        return [
            ID::make()->sortable(),
            Date::make('Fecha', 'date'),
            Select::make('Estado', 'status')
                  ->options([
                      'pendiente' => 'Pendiente',
                      'enviado'   => 'Enviado',
                      'entregado' => 'Entregado',
                      'cancelado' => 'Cancelado',
                  ]),
            BelongsTo::make('Cliente', 'client', resource: ClientResource::class),
        ];
    }
}

OrderFormPage.php — el campo clave: BelongsToMany con fields() para el pivot:

<?php
namespace App\MoonShine\Resources\Order\Pages;

use App\MoonShine\Resources\Client\ClientResource;
use App\MoonShine\Resources\Product\ProductResource;
use MoonShine\Laravel\Fields\Relationships\BelongsTo;
use MoonShine\Laravel\Fields\Relationships\BelongsToMany;
use MoonShine\Laravel\Pages\Crud\FormPage;
use MoonShine\UI\Fields\Date;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Number;
use MoonShine\UI\Fields\Select;

class OrderFormPage extends FormPage
{
    protected function fields(): iterable
    {
        return [
            ID::make(),
            Date::make('Fecha', 'date')->required(),
            Select::make('Estado', 'status')
                  ->options([
                      'pendiente' => 'Pendiente',
                      'enviado'   => 'Enviado',
                      'entregado' => 'Entregado',
                      'cancelado' => 'Cancelado',
                  ])
                  ->default('pendiente')
                  ->required(),
            BelongsTo::make('Cliente', 'client', resource: ClientResource::class)
                     ->required(),
            BelongsToMany::make('Productos', 'products', resource: ProductResource::class)
                         ->fields([
                             Number::make('Cantidad', 'quantity')->min(1)->default(1),
                         ]),
        ];
    }

    protected function rules(mixed $item): array
    {
        return [
            'date'      => ['required', 'date'],
            'status'    => ['required', 'in:pendiente,enviado,entregado,cancelado'],
            'client_id' => ['required', 'exists:clients,id'],
        ];
    }
}
El método ->fields([]) dentro de BelongsToMany define los campos extras de la tabla pivot. El nombre del campo ('quantity') debe coincidir exactamente con el nombre de la columna en order_product.

OrderDetailPage.php

<?php
namespace App\MoonShine\Resources\Order\Pages;

use App\MoonShine\Resources\Client\ClientResource;
use App\MoonShine\Resources\Product\ProductResource;
use MoonShine\Laravel\Fields\Relationships\BelongsTo;
use MoonShine\Laravel\Fields\Relationships\BelongsToMany;
use MoonShine\Laravel\Pages\Crud\DetailPage;
use MoonShine\UI\Fields\Date;
use MoonShine\UI\Fields\ID;
use MoonShine\UI\Fields\Number;
use MoonShine\UI\Fields\Select;

class OrderDetailPage extends DetailPage
{
    protected function fields(): iterable
    {
        return [
            ID::make(),
            Date::make('Fecha', 'date'),
            Select::make('Estado', 'status')
                  ->options([
                      'pendiente' => 'Pendiente',
                      'enviado'   => 'Enviado',
                      'entregado' => 'Entregado',
                      'cancelado' => 'Cancelado',
                  ]),
            BelongsTo::make('Cliente', 'client', resource: ClientResource::class),
            BelongsToMany::make('Productos', 'products', resource: ProductResource::class)
                         ->fields([
                             Number::make('Cantidad', 'quantity'),
                         ]),
        ];
    }
}
10

Service Provider

Registrá los cuatro resources en app/Providers/MoonShineServiceProvider.php:

<?php
namespace App\Providers;

use App\MoonShine\Resources\Category\CategoryResource;
use App\MoonShine\Resources\Client\ClientResource;
use App\MoonShine\Resources\Order\OrderResource;
use App\MoonShine\Resources\Product\ProductResource;
use Illuminate\Support\ServiceProvider;
use MoonShine\Contracts\Core\DependencyInjection\CoreContract;
use MoonShine\Laravel\DependencyInjection\ConfiguratorContract;
use MoonShine\Laravel\Resources\MoonShineUserResource;
use MoonShine\Laravel\Resources\MoonShineUserRoleResource;

class MoonShineServiceProvider extends ServiceProvider
{
    public function boot(
        CoreContract $core,
        ConfiguratorContract $config,
    ): void {
        $core
            ->resources([
                MoonShineUserResource::class,
                MoonShineUserRoleResource::class,
                CategoryResource::class,
                ClientResource::class,
                ProductResource::class,
                OrderResource::class,
            ])
            ->pages([
                ...$config->getPages(),
            ]);
    }
}
Alternativa con autoload — si no querés listar cada resource manualmente, reemplazá el contenido de boot() con $core->autoload();. MoonShine detecta automáticamente todos los Resources dentro de app/MoonShine/. En producción ejecutá php artisan optimize.
11

Menú en el Layout

En MoonShine 4 el menú del sidebar se define en el Layout, no en el ServiceProvider como en versiones anteriores. Abrí app/MoonShine/Layouts/MoonShineLayout.php y buscá el método menu():

use App\MoonShine\Resources\Category\CategoryResource;
use App\MoonShine\Resources\Client\ClientResource;
use App\MoonShine\Resources\Order\OrderResource;
use App\MoonShine\Resources\Product\ProductResource;
use MoonShine\Laravel\Resources\MoonShineUserResource;
use MoonShine\Laravel\Resources\MoonShineUserRoleResource;
use MoonShine\MenuManager\MenuGroup;
use MoonShine\MenuManager\MenuItem;

protected function menu(): array
{
    return [
        MenuGroup::make('Catálogo', [
            MenuItem::make('Categorías', CategoryResource::class)
                    ->icon('s.tag'),
            MenuItem::make('Productos', ProductResource::class)
                    ->icon('s.shopping-bag'),
        ])->icon('s.squares-2x2'),

        MenuGroup::make('Ventas', [
            MenuItem::make('Clientes', ClientResource::class)
                    ->icon('s.users'),
            MenuItem::make('Pedidos', OrderResource::class)
                    ->icon('s.clipboard-document-list'),
        ])->icon('s.chart-bar'),

        MenuGroup::make('Administración', [
            MenuItem::make('Usuarios', MoonShineUserResource::class)
                    ->icon('s.user'),
            MenuItem::make('Roles', MoonShineUserRoleResource::class)
                    ->icon('s.shield-check'),
        ])->icon('s.cog-6-tooth'),
    ];
}
MoonShine 4 usa Heroicons v2. Los prefijos son: s. = solid · o. = outline · m. = mini. Referencia completa en heroicons.com.
12

Seeder de prueba

Cargá datos de prueba en database/seeders/DatabaseSeeder.php:

<?php
namespace Database\Seeders;

use App\Models\Category;
use App\Models\Client;
use App\Models\Order;
use App\Models\Product;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        // Categorías
        $electronica = Category::create(['name' => 'Electrónica',  'description' => 'Dispositivos y gadgets']);
        $ropa        = Category::create(['name' => 'Ropa',         'description' => 'Prendas de vestir']);
        $alimentos   = Category::create(['name' => 'Alimentos',    'description' => 'Comida y bebidas']);

        // Productos
        $p1 = Product::create(['name' => 'Audífonos Bluetooth', 'price' => 89000,  'stock' => 15, 'category_id' => $electronica->id]);
        $p2 = Product::create(['name' => 'Camiseta Polo',       'price' => 45000,  'stock' => 30, 'category_id' => $ropa->id]);
        $p3 = Product::create(['name' => 'Café Tostado 500g',   'price' => 18000,  'stock' => 50, 'category_id' => $alimentos->id]);
        $p4 = Product::create(['name' => 'Teclado Mecánico',    'price' => 210000, 'stock' => 8,  'category_id' => $electronica->id]);

        // Clientes
        $c1 = Client::create(['name' => 'Ana Torres',  'email' => 'ana@correo.com',   'phone' => '3001234567']);
        $c2 = Client::create(['name' => 'Luis Gómez',  'email' => 'luis@correo.com',  'phone' => '3109876543']);
        $c3 = Client::create(['name' => 'María Pérez', 'email' => 'maria@correo.com', 'phone' => '3207654321']);

        // Pedidos con productos (relación pivot)
        $o1 = Order::create(['date' => '2025-01-15', 'status' => 'entregado', 'client_id' => $c1->id]);
        $o1->products()->attach([
            $p1->id => ['quantity' => 2],
            $p3->id => ['quantity' => 1],
        ]);

        $o2 = Order::create(['date' => '2025-01-20', 'status' => 'enviado', 'client_id' => $c2->id]);
        $o2->products()->attach([
            $p4->id => ['quantity' => 1],
            $p2->id => ['quantity' => 3],
        ]);

        $o3 = Order::create(['date' => '2025-01-22', 'status' => 'pendiente', 'client_id' => $c3->id]);
        $o3->products()->attach([
            $p2->id => ['quantity' => 2],
        ]);
    }
}
php artisan db:seed
13

Verificación y errores comunes

php artisan config:clear
php artisan cache:clear
php artisan serve

Abrí http://127.0.0.1:8000/moonshine e iniciá sesión con el usuario creado durante la instalación. Verificá esto:

  1. Sidebar muestra los grupos "Catálogo" y "Ventas" con íconos
  2. Categorías → detalle → tabla de productos de esa categoría (HasMany)
  3. Clientes → detalle → tabla de pedidos de ese cliente (HasMany)
  4. Productos → formulario → dropdown de categorías (BelongsTo)
  5. Pedidos → formulario → dropdown de clientes y selector múltiple de productos con campo cantidad (BelongsToMany + pivot)

Errores comunes:

ERROR: Class not found en los Pages de un resource
→ Los namespaces deben coincidir exactamente con la ruta del archivo.
  Archivo en Resources/Category/Pages/CategoryFormPage.php
  debe tener: namespace App\MoonShine\Resources\Category\Pages;

ERROR: Column not found: quantity (en el pivot)
→ Verificá que order_product tenga el campo 'quantity'.
→ Los modelos Product y Order deben tener withPivot('quantity').

ERROR: BelongsToMany no guarda los productos
→ Verificá withPivot('quantity') y withTimestamps() en ambos modelos.
→ El campo Number en el FormPage debe ser: Number::make('Cantidad', 'quantity')

ERROR: Menú no aparece o íconos en blanco
→ El método menu() va en MoonShineLayout.php, NO en el ServiceProvider.
→ Usá prefijos s. o. m. para los íconos (Heroicons v2).

ERROR: Resource no aparece en el panel
→ Verificá que esté listado en el boot() del ServiceProvider.
→ O usá $core->autoload() y ejecutá composer dump-autoload.
Código de este tutorial
Seguí los pasos de este post para construir el proyecto desde cero. El tutorial está basado en la documentación oficial de MoonShine 4: getmoonshine.app/en/docs/4.x
Volver al blog