Laravel и использование поля JSON в MySQL

Laravel и использование поля JSON в MySQL

Поля JSON становятся все более популярными, теперь они официально поддерживаются в MySQL 5.7.8. Их даже использует популярный пакет Spatie Laravel Medialibrary, так почему бы и нам не попробовать? В этом уроке мы покажем как это сделать.

Допустим, у нас есть интернет-магазина и нам нужно хранить в базе товары. Но мы не знаем какие у них будут свойства, для некоторых потребуются размеры, для других — цвет, страна и производитель и т.д. И вот здесь поле JSON может быть очень полезно — мы будем хранить там любые данные о свойствах товаров.

Вот форма для нашего товара:

Примечание : для упрощения я не сделал динамические поля в форме, это уже за пределами нашей статьи,  просто жестко задал 5 полей.

 

Шаг 1. Бэкэнд: Миграция + Модель

Чтобы создать поле JSON, нам нужно в миграции Laravel использовать метод ->json():

Schema::create('products', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->decimal('price', 15, 2);
$table->json('properties');
$table->timestamps();
$table->softDeletes();
});
Schema::create('products', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->decimal('price', 15, 2); $table->json('properties'); $table->timestamps(); $table->softDeletes(); });
Schema::create('products', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name');
    $table->decimal('price', 15, 2);
    $table->json('properties');
    $table->timestamps();
    $table->softDeletes();
});

Далее нам нужно указать нашей модели app/Product.php автоматически преобразовать это поле из JSON формата в массив:

class Product extends Model
{
protected $casts = [
'properties' => 'array'
];
class Product extends Model { protected $casts = [ 'properties' => 'array' ];
class Product extends Model
{
    protected $casts = [
        'properties' => 'array'
    ];

Таким образом, мы сразу получим $product->properties в виде массива, без необходимости конвертирования через json_decode().

 

Шаг 2. Форма Blade c массивом

Как я говорил выше, я добавил пять полей для свойств. Пользователь может заполнить как одно из них так и все пять:

<form action="{{ route("admin.products.store") }}" method="POST">
@csrf
<div class="form-group">
<label for="name">Name</label>
<input type="text" name="name" class="form-control">
</div>
<div class="form-group">
<label for="price">Price*</label>
<input type="number" name="price" class="form-control" step="0.01">
</div>
<div class="form-group">
<label for="properties">Properties</label>
<div class="row">
<div class="col-md-2">
Key:
</div>
<div class="col-md-4">
Value:
</div>
</div>
@for ($i=0; $i <= 4; $i++)
<div class="row">
<div class="col-md-2">
<input type="text" name="properties[{{ $i }}][key]" class="form-control" value="{{ old('properties['.$i.'][key]') }}">
</div>
<div class="col-md-4">
<input type="text" name="properties[{{ $i }}][value]" class="form-control" value="{{ old('properties['.$i.'][value]') }}">
</div>
</div>
@endfor
</div>
<div>
<input class=" btn btn-danger" type="submit">
</div>
</form>
<form action="{{ route("admin.products.store") }}" method="POST"> @csrf <div class="form-group"> <label for="name">Name</label> <input type="text" name="name" class="form-control"> </div> <div class="form-group"> <label for="price">Price*</label> <input type="number" name="price" class="form-control" step="0.01"> </div> <div class="form-group"> <label for="properties">Properties</label> <div class="row"> <div class="col-md-2"> Key: </div> <div class="col-md-4"> Value: </div> </div> @for ($i=0; $i <= 4; $i++) <div class="row"> <div class="col-md-2"> <input type="text" name="properties[{{ $i }}][key]" class="form-control" value="{{ old('properties['.$i.'][key]') }}"> </div> <div class="col-md-4"> <input type="text" name="properties[{{ $i }}][value]" class="form-control" value="{{ old('properties['.$i.'][value]') }}"> </div> </div> @endfor </div> <div> <input class=" btn btn-danger" type="submit"> </div> </form>
<form action="{{ route("admin.products.store") }}" method="POST">
    @csrf
    <div class="form-group">
        <label for="name">Name</label>
        <input type="text" name="name" class="form-control">
    </div>
    <div class="form-group">
        <label for="price">Price*</label>
        <input type="number" name="price" class="form-control" step="0.01">
    </div>
    <div class="form-group">
        <label for="properties">Properties</label>
        <div class="row">
            <div class="col-md-2">
                Key:
            </div>
            <div class="col-md-4">
                Value:
            </div>
        </div>
        @for ($i=0; $i <= 4; $i++)
        <div class="row">
            <div class="col-md-2">
                <input type="text" name="properties[{{ $i }}][key]" class="form-control" value="{{ old('properties['.$i.'][key]') }}">
            </div>
            <div class="col-md-4">
                <input type="text" name="properties[{{ $i }}][value]" class="form-control" value="{{ old('properties['.$i.'][value]') }}">
            </div>
        </div>
        @endfor
    </div>
    <div>
        <input class=" btn btn-danger" type="submit">
    </div>
</form>

Как видите, обычный цикл @for и пары ключ-значение в массиве от 0 до 4.

 

Шаг 3. Сохранение свойств

Наш метод ProductController::store() будет простым.

public function store(StoreProductRequest $request)
{
$product = Product::create($request->all());
return redirect()->route('admin.products.index');
}
public function store(StoreProductRequest $request) { $product = Product::create($request->all()); return redirect()->route('admin.products.index'); }
public function store(StoreProductRequest $request)
{
    $product = Product::create($request->all());
    return redirect()->route('admin.products.index');
}

Да, всё так. Валидация обязательных полей в StoreProductRequest, ничего сложного. Нам не нужно ничего делать с нашим полем JSON, так как мы передаем массив из Blade и он будет автоматически приведен к JSON.

Вот как это будет выглядеть в базе данных:

У нас только одна проблема. В этом примере я не проверяю пустые значения, поэтому они по-прежнему будут храниться в JSON, как показано ниже — посмотрите на два последних значения:

[
{"key":"Size","value":"XL"},
{"key":"Color","value":"Blue"},
{"key":"Country","value":"China"},
{"key":null,"value":null},
{"key":null,"value":null}
]
[ {"key":"Size","value":"XL"}, {"key":"Color","value":"Blue"}, {"key":"Country","value":"China"}, {"key":null,"value":null}, {"key":null,"value":null} ]
[
  {"key":"Size","value":"XL"},
  {"key":"Color","value":"Blue"},
  {"key":"Country","value":"China"},
  {"key":null,"value":null},
  {"key":null,"value":null}
]

Чтобы избежать этого, нам нужно исключить значения null из массива. Я использую мутатор Eloquent и преобразую массив в модели app/Product.php:

public function setPropertiesAttribute($value)
{
$properties = [];
foreach ($value as $array_item) {
if (!is_null($array_item['key'])) {
$properties[] = $array_item;
}
}
$this->attributes['properties'] = json_encode($properties);
}
public function setPropertiesAttribute($value) { $properties = []; foreach ($value as $array_item) { if (!is_null($array_item['key'])) { $properties[] = $array_item; } } $this->attributes['properties'] = json_encode($properties); }
public function setPropertiesAttribute($value)
{
    $properties = [];

    foreach ($value as $array_item) {
        if (!is_null($array_item['key'])) {
            $properties[] = $array_item;
        }
    }

    $this->attributes['properties'] = json_encode($properties);
}

Примечание: я догадываюсь, что есть более элегантные способы выполнения этой операции с массивом, но подумал, что в этой статье не очень уместно искать решение для массива в одну строку.

 

Шаг 4. Отображение свойств

В таблице товаров мы, вероятно, захотим показать что-то вроде этого:

Для этого мы просто сделаем @foreach в ячейке <td> файла resources/views/products/index.blade.php:

@foreach($products as $product)
<tr>
<td>
{{ $product->name ?? '' }}
</td>
<td>
{{ $product->price ?? '' }}
</td>
<td>
@foreach ($product->properties as $property)
<b>{{ $property['key'] }}</b>: {{ $property['value'] }}<br />
@endforeach
</td>
@foreach($products as $product) <tr> <td> {{ $product->name ?? '' }} </td> <td> {{ $product->price ?? '' }} </td> <td> @foreach ($product->properties as $property) <b>{{ $property['key'] }}</b>: {{ $property['value'] }}<br /> @endforeach </td>
@foreach($products as $product)
    <tr>
        <td>
            {{ $product->name ?? '' }}
        </td>
        <td>
            {{ $product->price ?? '' }}
        </td>
        <td>
            @foreach ($product->properties as $property)
                <b>{{ $property['key'] }}</b>: {{ $property['value'] }}<br />
            @endforeach
        </td>

 

Шаг 5. Редактирование/обновление свойств

Форма редактирования будет иметь такую же структуру, только значения полей будут установлены из массива. Вот часть файла resources/views/products/edit.blade.php:

<div class="form-group">
<label for="properties">Properties</label>
<div class="row">
<div class="col-md-2">
Key:
</div>
<div class="col-md-4">
Value:
</div>
</div>
@for ($i=0; $i <= 4; $i++)
<div class="row">
<div class="col-md-2">
<input type="text" name="properties[{{ $i }}][key]" class="form-control"
value="{{ $product->properties[$i]['key'] ?? '' }}">
</div>
<div class="col-md-4">
<input type="text" name="properties[{{ $i }}][value]" class="form-control"
value="{{ $product->properties[$i]['value'] ?? '' }}">
</div>
</div>
@endfor
</div>
<div class="form-group"> <label for="properties">Properties</label> <div class="row"> <div class="col-md-2"> Key: </div> <div class="col-md-4"> Value: </div> </div> @for ($i=0; $i <= 4; $i++) <div class="row"> <div class="col-md-2"> <input type="text" name="properties[{{ $i }}][key]" class="form-control" value="{{ $product->properties[$i]['key'] ?? '' }}"> </div> <div class="col-md-4"> <input type="text" name="properties[{{ $i }}][value]" class="form-control" value="{{ $product->properties[$i]['value'] ?? '' }}"> </div> </div> @endfor </div>
<div class="form-group">
    <label for="properties">Properties</label>
    <div class="row">
        <div class="col-md-2">
            Key:
        </div>
        <div class="col-md-4">
            Value:
        </div>
    </div>
    @for ($i=0; $i <= 4; $i++)
        <div class="row">
            <div class="col-md-2">
                <input type="text" name="properties[{{ $i }}][key]" class="form-control" 
                  value="{{ $product->properties[$i]['key'] ?? '' }}">
            </div>
            <div class="col-md-4">
                <input type="text" name="properties[{{ $i }}][value]" class="form-control" 
                  value="{{ $product->properties[$i]['value'] ?? '' }}">
            </div>
        </div>
    @endfor
</div>

В контроллере с помощью метода update(), а он очень похож на метод store() — мы просто используем все запросы для обновления данных.

public function update(UpdateProductRequest $request, Product $product)
{
$product->update($request->all());
return redirect()->route('admin.products.index');
}
public function update(UpdateProductRequest $request, Product $product) { $product->update($request->all()); return redirect()->route('admin.products.index'); }
public function update(UpdateProductRequest $request, Product $product)
{
    $product->update($request->all());
    return redirect()->route('admin.products.index');
}

Вот и всё, JSON — это просто, не так ли?

Автор: Povilas Korop
Перевод: Алексей Широков

Наш Телеграм-канал — следите за новостями о Laravel.