laravel делает довольно хорошую работу по прозрачному переводу моделей в json до тех пор, пока вы сохраняете простоту. однако эта простота использования довольно быстро разрушается, как только вы решаете внести значительные изменения в ваши представления json. если вы когда-нибудь сталкивались с борьбой hidden
против visible
, или возились с casts
или accessors, вы, вероятно, понимаете.
К счастью, laravel предоставляет простой способ определить json-сериализацию модели в одном методе: jsonSerialize()
.
зачем использовать jsonSerialize?
хотя это и не «канонический» способ сериализации, jsonSerialize()
обеспечивает как мощность, так и улучшенную читаемость.
Используя jsonSerialize()
, мы можем добавлять, удалять или изменять любое поле в нашей модели, не прибегая к смешиванию различных значений конфигурации. а поскольку наше определение json находится в одном методе, наш код стал немного более читабельным.
пролет
мы можем многое сделать с помощью jsonSerialize
, но в этой статье мы рассмотрим только основы, а именно:
- простая реализация для изменения структуры json и возвращаемых данных
- включение отношений в наш json
- подклассификация моделей для создания новых схем сериализации
структура данных
поскольку здесь мы работаем с моделями, мы собираемся быстро объяснить проект и структуру данных для последующих примеров.
Наш проект — это простой api для просмотра коллекции виниловых пластинок. структура данных состоит из records
, причем каждая запись имеет еще одну songs
.
api имеет три конечные точки:
миграции для наших таблиц показаны в нижней части статьи.
в остальной части статьи мы рассмотрим модели и контроллеры.
простой формат json с помощью jsonSerialize
первая модель, которую мы рассмотрим, это Song.php
.
До добавления jsonSerialize
модель выглядела следующим образом:
app/Models/Song.php
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
class Song extends Model
{
use HasFactory;
protected $table = 'songs';
}
когда мы вызываем конечную точку GET /api/songs
, чтобы вернуть все песни в нашей базе данных, результат выглядит следующим образом:
[
{
"id": 1,
"record_id": 1,
"title": "Bait and Switch",
"duration_seconds": 148,
"created_at": "2022-05-02T22:32:01.000000Z",
"updated_at": "2022-05-02T22:32:01.000000Z"
},
{
"id": 2,
"record_id": 1,
"title": "Sneaking into yer House",
"duration_seconds": 80,
"created_at": "2022-05-02T22:32:14.000000Z",
"updated_at": "2022-05-02T22:32:14.000000Z"
},
{
"id": 3,
"record_id": 1,
"title": "Dippity Do-Nut",
"duration_seconds": 112,
"created_at": "2022-05-03T11:16:25.000000Z",
"updated_at": "2022-05-03T11:16:25.000000Z"
}
]
Это не очень хороший результат. Давайте исправим это с помощью jsonSerialize()
!
В нижней части нашей модели Song
мы добавим этот метод:
/**
* Define the json format of the model
*/
public function jsonSerialize():Array
{
return [
'id' => $this->id,
'title' => $this->title,
// cast seconds to minutes and seconds
'duration' => gmdate("i:s", $this->duration_seconds),
// a new value
'long_enough_for_radio_play' => $this->duration_seconds > 120 ? true: false,
];
} // jsonSerialize
давайте посмотрим, что здесь происходит.
Сначала мы объявим публичный метод jsonSerialize()
. Этот метод вызывается каждый раз, когда эта модель сериализуется в json как для отдельных объектов, так и для коллекций.
В примере выше мы явно включили id
и title
, но опустили обе временные метки записи. В результате id
и title
отображаются в нашем json, но, например, created_at
нет.
Мы также видим, что применили преобразование к столбцу duration_seconds
для преобразования секунд в минуты и секунды, присвоив его ключу duration
, и добавили новый ключ/значение long_enough_for_radio_play
, чтобы указать, что песня длится более двух минут.
Это один из самых мощных аспектов jsonSerialize()
: мы можем применять приведения и преобразования к любому столбцу, переименовывать ключи и даже вводить новые пары ключ/значение.
Когда мы снова вызываем GET /api/songs
, наш вывод json выглядит следующим образом:
[
{
"id": 1,
"title": "Bait and Switch",
"duration": "02:28",
"long_enough_for_radio_play": true
},
...
]
Наши новые ключи на месте, а временные метки исчезли!
составные объекты с помощью jsonSerialize
До сих пор мы рассматривали сериализацию простых объектов. теперь перейдем к составным объектам: объектам, которые содержат другие объекты.
Давайте рассмотрим модель для Record
:
app/Models/Record.php
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentRelationsHasMany;
class Record extends Model
{
use HasFactory;
protected $table = 'records';
/**
* Returns all songs for record
*
* @return HasMany
*/
protected function songs():HasMany
{
return $this->hasMany('AppModelsSong', 'record_id', 'id')->orderBy('id', 'asc');
}
/**
* Define the json format of the model
*/
public function jsonSerialize():Array
{
return [
'id' => $this->id,
'artist' => $this->artist,
'title' => $this->title,
'songs' => $this->songs, // array of songs
];
} // jsonSerialize
}
Поскольку записи содержат песни, мы хотим, чтобы наша модель Record
включала массив моделей Song
этой записи. Для этого нужно сделать две вещи.
Во-первых, мы напишем функцию songs()
. Эта функция создает отношение hasMany()
к songs
и возвращает его. Если вы уже много работали с eloquent, это должно быть знакомо.
Затем, в нашем методе jsonSerialize()
, мы включаем эти песни, вызывая метод songs()
. В результате мы получаем массив песен с ключом songs
.
Следует отметить, что формат json всех этих песен определяется собственным методом Song
модели jsonSerialize()
. очень удобно!
Как только мы это напишем, наша конечная точка GET /api/records/{id}
, которая возвращает Record::all()
, даст нам результат, который выглядит следующим образом:
{
"id": 1,
"artist": "Emily's Sassy Lime",
"title": "Dippity Do-Nut",
"songs": [
{
"id": 1,
"title": "Bait and Switch",
"duration": "02:28",
"long_enough_for_radio_play": true
},
{
"id": 2,
"title": "Sneaking into yer House",
"duration": "01:20",
"long_enough_for_radio_play": false
},
{
"id": 3,
"title": "Dippity Do-Nut",
"duration": "01:52",
"long_enough_for_radio_play": false
}
]
}
подклассификация для разных форматов
иногда нам нужны две разные схемы json для одной и той же модели. например, если мы хотим написать конечную точку, которая возвращает список всех наших записей, нам, вероятно, нужен массив более мелких объектов; «дайджест». возможно, нам нужны только поля artist
, title
и id
.
Мы можем сделать это, подклассифицировав нашу модель Record
и перегрузив jsonSerialize
. Сделав нашу новую модель, назовем ее RecordDigest
, подклассом Record
, мы сможем получить доступ ко всем методам и данным суперкласса, если захотим, но сериализация json по умолчанию будет нашей новой моделью. давайте посмотрим на модель RecordDigest
.
app/Models/RecordDigest.php
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentRelationsHasMany;
class RecordDigest extends Record
{
/**
* Define the json format of the model
*/
public function jsonSerialize():Array
{
return [
'id' => $this->id,
'artist' => $this->artist,
'title' => $this->title,
];
} // jsonSerialize
}
Это совершенно новая модель, но поскольку она наследуется от Record
, она по-прежнему обращается к таблице records
, и у нас по-прежнему есть доступ к методу songs()
, если мы захотим его использовать.
Если мы напишем конечную точку GET /api/records
, которая получит RecordDigest::all()
, мы увидим, что наш вывод json выглядит следующим образом:
[
{
"id": 1,
"artist": "Emily's Sassy Lime",
"title": "Dippity Do-Nut"
},
{
"id": 2,
"artist": "Bratmobile",
"title": "Pottymouth"
}
]
именно то, что мы хотели.
заключение
С помощью jsonSerialize()
можно сделать многое. Я часто использую его как способ введения данных hateoas в мои возвраты, или для адаптации данных или структуры json в зависимости, скажем, от роли доступа пользователя. Создавая такие формализованные способы определения нашего json, мы получаем не только мощь, но и последовательность и улучшенную читабельность.
дополнительный код
если вы хотите подробнее изучить этот пример, исходный текст приведен здесь:
models
Song.php
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
class Song extends Model
{
use HasFactory;
protected $table = 'songs';
/**
* Define the json format of the model
*/
public function jsonSerialize():Array
{
return [
'id' => $this->id,
'title' => $this->title,
// cast seconds to minutes and seconds
'duration' => gmdate("i:s", $this->duration_seconds),
];
} // jsonSerialize
}
Record.php
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentRelationsHasMany;
class Record extends Model
{
use HasFactory;
protected $table = 'records';
/**
* Returns all songs for record
*
* @return HasMany
*/
protected function songs():HasMany
{
return $this->hasMany('AppModelsSong', 'record_id', 'id')->orderBy('id', 'desc');
}
/**
* Define the json format of the model
*/
public function jsonSerialize():Array
{
return [
'id' => $this->id,
'artist' => $this->artist,
'title' => $this->title,
'songs' => $this->songs, // array of songs
];
} // jsonSerialize
}
RecordDigest.php
<?php
namespace AppModels;
use IlluminateDatabaseEloquentFactoriesHasFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateDatabaseEloquentRelationsHasMany;
class RecordDigest extends Record
{
/**
* Define the json format of the model
*/
public function jsonSerialize():Array
{
return [
'id' => $this->id,
'artist' => $this->artist,
'title' => $this->title,
];
} // jsonSerialize
}
контроллер
RecordController.php
<?php
namespace AppHttpControllersapi;
use IlluminateHttpRequest;
use IlluminateHttpJsonResponse;
use AppHttpControllersController;
use AppModelsSong;
use AppModelsRecord;
use AppModelsRecordDigest;
class RecordController extends Controller
{
/**
* Get all songs
*
* @return JsonResponse
*/
public function getAllSongs(Request $request):JsonResponse
{
$songs = Song::all();
return response()->json($songs, 200);
}
/**
* Get one record
*
* @return JsonResponse
*/
public function getRecord(Int $id, Request $request):JsonResponse
{
$records = Record::find($id);
return response()->json($records, 200);
}
/**
* Get all records as digests
*
* @return JsonResponse
*/
public function getAllRecords(Request $request):JsonResponse
{
$records = RecordDigest::all();
return response()->json($records, 200);
}
маршруты
routes/api.php
Route::get('songs', 'AppHttpControllersapiRecordController@getAllSongs');
Route::get('records', 'AppHttpControllersapiRecordController@getAllRecords');
Route::get('records/{id}', 'AppHttpControllersapiRecordController@getRecord');
миграции
миграция записей
<?php
use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;
class CreateRecordsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('records', function (Blueprint $table) {
$table->id();
$table->string('artist');
$table->string('title');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('records');
}
}
миграция песен
<?php
use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;
class CreateSongsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('songs', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('record_id');
$table->foreign('record_id')->references('id')->on('records');
$table->string("title");
$table->integer("duration_seconds");
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('songs');
}
}