Как вы сегодня, ребята?

В этой части мы создадим CRUD для данных о продукте. Думаю, в этой части я не буду слишком много делиться теорией, но чаще буду делиться своим кодом. Потому что это не слишком отличается от учебника в категории продуктов.

Вы готовы?

[Шаг 1]: Добавить маршрут

Мы просто модифицируем файл routes/web.php следующим образом:

<?php
use App\Http\Livewire\Category;
use GuzzleHttp\Middleware;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
    return view('welcome');
});
Route::middleware(['auth:sanctum', 'verified'])->group(function () {
    Route::get('/dashboard', function () {
        return view('dashboard');
    })->name('dashboard');
    Route::get('/categories', function () {
        return view('admin.categories');
    })->name('categories');
    Route::get('/products', function () {
        return view('admin.products');
    })->name('products');
});

[Шаг 2]: Создайте и измените файл Livewire Controller для продукта.

Создайте файл Livewire Controller для покрытия данных продукта CRUD, создайте с помощью cmd, как и в предыдущем случае:

php artisan make:livewire Product

Затем измените файл app/http/Livewire/Product.php следующим образом:

<?php
namespace App\Http\Livewire;
use App\Models\Product as ModelsProduct;
use App\Models\ProductCategory;
use Livewire\Component;
use Livewire\WithPagination;
class Product extends Component
{
    use WithPagination;
    public $visibleModalForm = false;
    public $confirmDeleteModal = false;
    public $name;
    public $price;
    public $description;
    public $tags;
    public $slug;
    public $categories_id;
    public $categories;
    public $modelId;
    public function rules()
    {
        return [
            'name' => 'required',
            'slug' => 'required',
            'price' => 'required',
            'description' => 'required',
            'categories_id' => 'required'
        ];
    }
    public function updatedName($value)
    {
        $this->generateSlug($value);
    }
    public function mount()
    {
        $this->resetPage();
    }
    public function read()
    {
        return ModelsProduct::orderBy('created_at', 'desc')->paginate(5);
    }
    public function createOrUpdate()
    {
        $this->validate();
        ModelsProduct::updateOrCreate(['id' => $this->modelId], $this->modelData());
        $this->visibleModalForm = false;
        $this->resetFields();
    }
    public function delete()
    {
        ModelsProduct::destroy($this->modelId);
        $this->confirmDeleteModal = false;
    }
    public function showUpdateModal($id)
    {
        $this->resetValidation();
        $this->resetFields();
        $this->modelId = $id;
        $this->visibleModalForm = true;
        $this->loadModel();
    }
    public function showDeleteModal($id)
    {
        $this->modelId = $id;
        $this->confirmDeleteModal = true;
    }
    public function loadModel()
    {
        $data = ModelsProduct::findOrFail($this->modelId);
        $this->name = $data->name;
        $this->slug = $data->slug;
        $this->price = $data->price;
        $this->description = $data->description;
        $this->tags = $data->tags;
        $this->categories_id = $data->categories_id;
        $this->categories = ProductCategory::all();
    }
    public function modelData()
    {
        return [
            'name' => $this->name,
            'slug' => $this->slug,
            'price' => $this->price,
            'description' => $this->description,
            'tags' => $this->tags,
            'categories_id' => $this->categories_id
        ];
    }
    public function resetFields()
    {
        $this->modelId = null;
        $this->name = null;
        $this->slug = null;
        $this->price = null;
        $this->description = null;
        $this->tags = null;
        $this->categories_id = null;
    }
    private function generateSlug($value)
    {
        $dash = str_replace(' ', '-', $value);
        $lower = strtolower($dash);
        $this->slug = $lower;
    }
    public function showModal()
    {
        $this->resetValidation();
        $this->resetFields();
        $this->categories = ProductCategory::all();
        $this->visibleModalForm = true;
    }
    public function render()
    {
        return view('livewire.product', [
            'data' => $this->read(),
            'categories' => $this->categories
        ]);
    }
}

[Шаг 3]: Добавить панель навигации по ссылкам

Чтобы добавить ссылку на панель навигации для продукта, измените файл "resources\views\navigation-menu.blade.php" следующим образом:

<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
    <!-- Primary Navigation Menu -->
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between h-16">
            <div class="flex">
                <!-- Logo -->
                <div class="flex-shrink-0 flex items-center">
                    <a href="{{ route('dashboard') }}">
                        <x-jet-application-mark class="block h-9 w-auto" />
                    </a>
                </div>
                <!-- Navigation Links -->
                <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
                    <x-jet-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
                        {{ __('Dashboard') }}
                    </x-jet-nav-link>
                    <x-jet-nav-link href="{{ route('categories') }}" :active="request()->routeIs('categories')">
                        {{ __('Categories') }}
                    </x-jet-nav-link>
                    <x-jet-nav-link href="{{ route('products') }}" :active="request()->routeIs('products')">
                        {{ __('Products') }}
                    </x-jet-nav-link>
                </div>
            </div>
            <div class="hidden sm:flex sm:items-center sm:ml-6">
                <!-- Teams Dropdown -->
                @if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
                <div class="ml-3 relative">
                    <x-jet-dropdown align="right" width="60">
                        <x-slot name="trigger">
                            <span class="inline-flex rounded-md">
                                <button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:bg-gray-50 hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition">
                                    {{ Auth::user()->currentTeam->name }}
                                    <svg class="ml-2 -mr-0.5 h-4 w-4" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                                        <path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
                                    </svg>
                                </button>
                            </span>
                        </x-slot>
                        <x-slot name="content">
                            <div class="w-60">
                                <!-- Team Management -->
                                <div class="block px-4 py-2 text-xs text-gray-400">
                                    {{ __('Manage Team') }}
                                </div>
                                <!-- Team Settings -->
                                <x-jet-dropdown-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}">
                                    {{ __('Team Settings') }}
                                </x-jet-dropdown-link>
                                @can('create', Laravel\Jetstream\Jetstream::newTeamModel())
                                <x-jet-dropdown-link href="{{ route('teams.create') }}">
                                    {{ __('Create New Team') }}
                                </x-jet-dropdown-link>
                                @endcan
                                <div class="border-t border-gray-100"></div>
                                <!-- Team Switcher -->
                                <div class="block px-4 py-2 text-xs text-gray-400">
                                    {{ __('Switch Teams') }}
                                </div>
                                @foreach (Auth::user()->allTeams() as $team)
                                <x-jet-switchable-team :team="$team" />
                                @endforeach
                            </div>
                        </x-slot>
                    </x-jet-dropdown>
                </div>
                @endif
                <!-- Settings Dropdown -->
                <div class="ml-3 relative">
                    <x-jet-dropdown align="right" width="48">
                        <x-slot name="trigger">
                            @if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
                            <button class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-gray-300 transition">
                                <img class="h-8 w-8 rounded-full object-cover" src="{{ Auth::user()->profile_photo_url }}" alt="{{ Auth::user()->name }}" />
                            </button>
                            @else
                            <span class="inline-flex rounded-md">
                                <button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition">
                                    {{ Auth::user()->name }}
                                    <svg class="ml-2 -mr-0.5 h-4 w-4" xmlns="https://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
                                        <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
                                    </svg>
                                </button>
                            </span>
                            @endif
                        </x-slot>
                        <x-slot name="content">
                            <!-- Account Management -->
                            <div class="block px-4 py-2 text-xs text-gray-400">
                                {{ __('Manage Account') }}
                            </div>
                            <x-jet-dropdown-link href="{{ route('profile.show') }}">
                                {{ __('Profile') }}
                            </x-jet-dropdown-link>
                            @if (Laravel\Jetstream\Jetstream::hasApiFeatures())
                            <x-jet-dropdown-link href="{{ route('api-tokens.index') }}">
                                {{ __('API Tokens') }}
                            </x-jet-dropdown-link>
                            @endif
                            <div class="border-t border-gray-100"></div>
                            <!-- Authentication -->
                            <form method="POST" action="{{ route('logout') }}">
                                @csrf
                                <x-jet-dropdown-link href="{{ route('logout') }}" onclick="event.preventDefault();
                                                this.closest('form').submit();">
                                    {{ __('Log Out') }}
                                </x-jet-dropdown-link>
                            </form>
                        </x-slot>
                    </x-jet-dropdown>
                </div>
            </div>
            <!-- Hamburger -->
            <div class="-mr-2 flex items-center sm:hidden">
                <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition">
                    <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
                        <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
                        <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                    </svg>
                </button>
            </div>
        </div>
    </div>
    <!-- Responsive Navigation Menu -->
    <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
        <div class="pt-2 pb-3 space-y-1">
            <x-jet-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
                {{ __('Dashboard') }}
            </x-jet-responsive-nav-link>
        </div>
        <!-- Responsive Settings Options -->
        <div class="pt-4 pb-1 border-t border-gray-200">
            <div class="flex items-center px-4">
                @if (Laravel\Jetstream\Jetstream::managesProfilePhotos())
                <div class="flex-shrink-0 mr-3">
                    <img class="h-10 w-10 rounded-full object-cover" src="{{ Auth::user()->profile_photo_url }}" alt="{{ Auth::user()->name }}" />
                </div>
                @endif
                <div>
                    <div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
                    <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
                </div>
            </div>
            <div class="mt-3 space-y-1">
                <!-- Account Management -->
                <x-jet-responsive-nav-link href="{{ route('profile.show') }}" :active="request()->routeIs('profile.show')">
                    {{ __('Profile') }}
                </x-jet-responsive-nav-link>
                @if (Laravel\Jetstream\Jetstream::hasApiFeatures())
                <x-jet-responsive-nav-link href="{{ route('api-tokens.index') }}" :active="request()->routeIs('api-tokens.index')">
                    {{ __('API Tokens') }}
                </x-jet-responsive-nav-link>
                @endif
                <!-- Authentication -->
                <form method="POST" action="{{ route('logout') }}">
                    @csrf
                    <x-jet-responsive-nav-link href="{{ route('logout') }}" onclick="event.preventDefault();
                                    this.closest('form').submit();">
                        {{ __('Log Out') }}
                    </x-jet-responsive-nav-link>
                </form>
                <!-- Team Management -->
                @if (Laravel\Jetstream\Jetstream::hasTeamFeatures())
                <div class="border-t border-gray-200"></div>
                <div class="block px-4 py-2 text-xs text-gray-400">
                    {{ __('Manage Team') }}
                </div>
                <!-- Team Settings -->
                <x-jet-responsive-nav-link href="{{ route('teams.show', Auth::user()->currentTeam->id) }}" :active="request()->routeIs('teams.show')">
                    {{ __('Team Settings') }}
                </x-jet-responsive-nav-link>
                @can('create', Laravel\Jetstream\Jetstream::newTeamModel())
                <x-jet-responsive-nav-link href="{{ route('teams.create') }}" :active="request()->routeIs('teams.create')">
                    {{ __('Create New Team') }}
                </x-jet-responsive-nav-link>
                @endcan
                <div class="border-t border-gray-200"></div>
                <!-- Team Switcher -->
                <div class="block px-4 py-2 text-xs text-gray-400">
                    {{ __('Switch Teams') }}
                </div>
                @foreach (Auth::user()->allTeams() as $team)
                <x-jet-switchable-team :team="$team" component="jet-responsive-nav-link" />
                @endforeach
                @endif
            </div>
        </div>
    </div>
</nav>

[Шаг 4]: Создание и изменение вида

Создайте файл products.blade.php в каталоге "resources\views\admin" и заполните этот код:

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Products') }}
        </h2>
    </x-slot>
    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
                @livewire('product')
            </div>
        </div>
    </div>
</x-app-layout>

Затем измените файл «C:\xampp\htdocs\myshop_backend\resources\views\livewire\product.blade.php» следующим образом:

<div class="p-6">
    <div class="flex items-center px-4 py-3 text-right sm:px-6">
        <x-jet-button wire:click="showModal">
            {{ __('Add Product') }}
        </x-jet-button>
    </div>
    <!-- Datatables -->
    <div class="flex flex-col">
        <div class="my-2 overflow-x-auto sm:mx-6 lg:mx-8">
            <div class="py-2 align-middle inline-block w-full sm:px-6 lg:px-8">
                <div class="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
                    <table class="w-full divide-y divide-gray-200">
                        <thead>
                            <tr>
                                <th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Name</th>
                                <th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Slug</th>
                                <th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Category</th>
                                <th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Price</th>
                                <th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Description</th>
                                <th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Tags</th>
                                <th class="px-6 py-3 bg-gray-100 text-center text-sm leading-4 font-medium text-gray-500 upercase tracking-wider">Actions</th>
                            </tr>
                        </thead>
                        <tbody class="bg-white divide-y divide-gray-200">
                            @if ($data->count())
                            @foreach ($data as $item)
                            <tr>
                                <td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->name }}</td>
                                <td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->slug }}</td>
                                <td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->category->name }}</td>
                                <td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->price }}</td>
                                <td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->description }}</td>
                                <td class="px-6 py-4 text-center text-base whitespace-no-wrap">{{ $item->tags }}</td>
                                <td class="px-6 py-4 text-center text-base">
                                    <x-jet-button wire:click="showUpdateModal({{ $item->id }})">
                                        {{ __('Update') }}
                                    </x-jet-button>
                                    <x-jet-danger-button class="ml-2" wire:click="showDeleteModal({{ $item->id }})" wire:loading.attr="disabled">
                                        {{ __('Delete') }}
                                    </x-jet-danger-button>
                                </td>
                            </tr>
                            @endforeach
                            @else
                            <tr>
                                <td class="px-6 py-4 text-center text-base whitespace-no-wrap" colspan="4">No Result Found</td>
                            </tr>
                            @endif
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
    <br />
    {{ $data->links() }}
    <!-- Show Modal -->
    <x-jet-dialog-modal wire:model="visibleModalForm">
        <x-slot name="title">
            {{ __('Form Add Product') }}
        </x-slot>
        <x-slot name="content">
            <div class="mt-4">
                <x-jet-label for="name" value="{{ __('Product Name') }}" />
                <x-jet-input type="text" class="mt-1 block w-3/4" placeholder="{{ __('Product Name') }}" wire:model="name" />
                <x-jet-input-error for="name" class="mt-2" />
            </div>
            <div class="mt-4">
                <x-jet-label for="title" value="{{ __('Slug') }}" />
                <div class="mt-1 flex w-3/4">
                    <span class="inline-flex items-center px-3 rounded-l-md border border-r-0 border-gray-300 bg-gray-50 text-gray-500 text-sm">
                        https://localhost:8000/
                    </span>
                    <input type="text" wire:model="slug" class="mt-1 block w-3/4 form-control border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-r-md shadow-sm" placeholder="url-slug" readonly />
                </div>
                <x-jet-input-error for="slug" class="mt-2" />
            </div>
            <div class="mt-4">
                <x-jet-label for="price" value="{{ __('Price') }}" />
                <x-jet-input type="text" class="mt-1 block w-3/4" placeholder="{{ __('Price') }}" wire:model.defer="price" />
                <x-jet-input-error for="price" class="mt-2" />
            </div>
            <div class="mt-4">
                <x-jet-label for="categories_id" value="{{ __('Category') }}" />
                <select wire:model.defer="categories_id" class="js-example-basic-single mt-1 block w-3/4 form-control border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm">
                    <option selected>-- Choice Categories --</option>
                    @if($categories)
                    @foreach($categories as $cat)
                    @if($cat->id == $categories_id)
                    <option value="{{ $cat->id }}" selected>{{ $cat->name }}</option>
                    @else
                    <option value="{{ $cat->id }}">{{ $cat->name }}</option>
                    @endif
                    @endforeach
                    @endif
                </select>
                <x-jet-input-error for="categories_id" class="mt-2" />
            </div>
            <div class="mt-4">
                <x-jet-label for="description" value="{{ __('Description') }}" />
                <textarea wire:model.defer="description" placeholder="Description" class="mt-1 block w-3/4 form-control border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm">
                </textarea>
                <x-jet-input-error for="description" class="mt-2" />
            </div>
        </x-slot>
        <x-slot name="footer">
            <x-jet-secondary-button wire:click="$toggle('visibleModalForm')">
                {{ __('Cancel') }}
            </x-jet-secondary-button>
            @if ($modelId)
            <x-jet-danger-button class="ml-2" wire:click="createOrUpdate" wire:loading.attr="disabled">
                {{ __('Update Product') }}
            </x-jet-danger-button>
            @else
            <x-jet-danger-button class="ml-2" wire:click="createOrUpdate" wire:loading.attr="disabled">
                {{ __('Add Product') }}
            </x-jet-danger-button>
            @endif
        </x-slot>
    </x-jet-dialog-modal>
    <!-- Show Delete Modal -->
    <x-jet-dialog-modal wire:model="confirmDeleteModal">
        <x-slot name="title">
            {{ __('Delete Product') }}
        </x-slot>
        <x-slot name="content">
            {{ __('Are you sure you want to delete this Product data ? Once your data is deleted, all of its resources and data will be permanently deleted.') }}
        </x-slot>
        <x-slot name="footer">
            <x-jet-secondary-button wire:click="$toggle('confirmDeleteModal')">
                {{ __('Cancel') }}
            </x-jet-secondary-button>
            <x-jet-danger-button class="ml-2" wire:click="delete" wire:loading.attr="disabled">
                {{ __('Delete') }}
            </x-jet-danger-button>
        </x-slot>
    </x-jet-dialog-modal>
</div>

Я думаю, что все готово, мы просто проверяем в браузере, что он работает успешно?

[Шаг 5]: Запустите и проверьте проект

Запустим проект в браузере.

Попробуйте создать продукт:

Слаг поля будет заполнен автоматически. а затем нажмите Добавить продукт

Ага.. Успешно. Затем попробуйте обновить данные

Измените цену и обновите.

Хорошо, он создан правильно. Далее, как насчет удаления? Давайте попробуем проверить.

И ты…

Большой !!! Успешно !!!

В следующей части я попытаюсь создать API, который будет использоваться внешним интерфейсом, таким как React js и Flutter для мобильных устройств.