Автоматическое конвертирование JSON в PHP-объекты

Доброго времени суток! В данной статье я покажу Вам один из способов автоматической конвертации
JSON строки в объекты PHP
. Т.е. представим следующую ситуацию — у нас есть некий API-сервис
(в нашем примере Jsonplaceholder), запросы к которому отдают ответы в JSON-формате.
Эти ответы потом нужно преобразовать в нечто такое, с чем мы дальнейшем можем работать в PHP.
Но благодаря библиотеке square/pjson и аттрибутам PHP мы можем максимально автоматизировать этот
процесс.

Итак, структура проекта следующая:


.
├── src
│   ├── Api
│   │   ├── Api.php
│   │   ├── Endpoint.php
│   │   ├── PhotosEndpoint.php
│   │   ├── PostEndpoint.php
│   │   └── UserEndpoint.php
│   ├── Objects
│   │   ├── Address.php
│   │   ├── Comment.php
│   │   ├── Company.php
│   │   ├── Geo.php
│   │   ├── Photo.php
│   │   ├── Post.php
│   │   └── User.php
│   └── index.php
├── composer.json
├── composer.lock

В папке src/Api у нас будут классы отвечающие за сетевое общение с разными сущностями, будь то статья (Post),
пользователь (User) и т.д. В папке src/Objects — сами объекты, в которые мы будем конвертировать JSON-ответ.

Итак, разберем содержимое каждой папки по порядку.

src/Objects/Post.php


<?php

namespace Objects;

// импортируем классы из библиотеки
use SquarePjsonJson;
use SquarePjsonJsonSerialize;

class Post
{
    // добавляем методы из трейта в класс Post
    use JsonSerialize;

    // каждое свойство класса Post соответствует свойству из JSON ответа
    // аттрибут #[Json] обязателен 
    #[Json]
    public int $id;

    #[Json]
    public int $userId;

    #[Json]
    public string $title;

    #[Json]
    public string $body;
}

Класс выше описывает следующий JSON-ответ:


{
    "id": 1,
    "userId": 1,
    "title": "Post title",
    "body": "Post text"
}

Т.е. как видно из примера, когда мы получим ответ от сервера, значения полей этого JSON-ответа станут значениями полей класса
автоматически. Причем, типы данных полей класса могут быть не только встроенными в PHP типами, но и пользовательскими типами —
классами. Пример ниже:


// User.php

<?php

namespace Objects;

use SquarePjsonJson;
use SquarePjsonJsonSerialize;

class User
{
    use JsonSerialize;

    #[Json]
    public int $id;

    #[Json]
    public string $name;

    #[Json]
    public string $username;

    #[Json]
    public string $email;

    #[Json]
    public Address $address;

    #[Json]
    public string $phone;

    #[Json]
    public string $website;

    #[Json]
    public Company $company;
}

class Company
{
    use JsonSerialize;

    #[Json]
    public string $name;

    #[Json]
    public string $catchPhrase;

    #[Json]
    public string $bs;
}

class Address
{
    use JsonSerialize;

    #[Json]
    public string $street;

    #[Json]
    public string $suite;

    #[Json]
    public string $city;

    #[Json]
    public string $zipcode;

    #[Json]
    public Geo $geo;
}

class Geo
{
    use JsonSerialize;

    #[Json]
    public float $lat;

    #[Json]
    public float $lng;
}

Теперь рассмотрим классы, отвечающие за взаимодействие с сетью и преобразование JSON-ответов в соответствуюшие объекты.
Базовый класс ApiEndpoint заключает в себе базовую логику работы с сетью. В его задачи входит обратиться к удаленному
серверу и получить JSON-ответ.

src/Api/Endpoint.php


<?php

namespace Api;

use Exception;
use ObjectsComment;
use ObjectsPhoto;
use ObjectsPost;

class Endpoint
{
    /**
     * @throws Exception
     */
    protected function endpoint(string $part): bool|string
    {
        $url = sprintf('%s/%s', 'https://jsonplaceholder.typicode.com', $part);
        return $this->fetch($url);
    }

    /**
     * @throws Exception
     */
    private function fetch($uri): string
    {
        $handle = curl_init();

        curl_setopt($handle, CURLOPT_URL, $uri);
        curl_setopt($handle, CURLOPT_POST, false);
        curl_setopt($handle, CURLOPT_HEADER, true);
        curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 10);

        $response      = curl_exec($handle);
        $headerLength  = curl_getinfo($handle, CURLINFO_HEADER_SIZE);
        $httpCode      = curl_getinfo($handle, CURLINFO_HTTP_CODE);
        $body          = substr($response, $headerLength);

        if ($httpCode != 200) {
            throw new Exception('Network problem', $httpCode);
        }

        return $body;
    }
}

Остальные классы из пространства Api наследуются от него:

ApiPhotosEndpoint


<?php

namespace Api;

use Exception;
use ObjectsPhoto;

class PhotosEndpoint extends Endpoint
{

    /**
     * @return array<Photo>
     * @throws Exception
     */
    public function getPhotos(int $limit = 10): array
    {
        return array_slice(Photo::listFromJsonString($this->endpoint('photos')), 0, $limit);
    }

}

ApiPostEndpoint


<?php

namespace Api;

use Exception;
use ObjectsComment;
use ObjectsPost;

class PostEndpoint extends Endpoint
{
    /**
     * @return array<Post>
     * @throws Exception
     */
    public function getPosts(int $limit = 10): array
    {
        return array_slice(Post::listFromJsonString($this->endpoint('posts')), 0, $limit);
    }

    /**
     * @param int $id
     * @return Post
     * @throws Exception
     */
    public function getPost(int $id): Post
    {
        return Post::fromJsonString($this->endpoint(sprintf('posts/%d', $id)));
    }

    /**
     * @return array<Comment>
     * @throws Exception
     */
    public function getPostComments(int $id): array
    {
        return Comment::listFromJsonString($this->endpoint(sprintf('posts/%d/comments', $id)));
    }
}

Также есть класс ApiApi, который используется как центральное место для доступа ко всем API-endpoint:


<?php

namespace Api;

class Api
{
    public static function posts()
    {
        return new PostEndpoint();
    }

    public static function photos()
    {
        return new Endpoint();
    }

    public static function users()
    {
        return new UserEndpoint();
    }
}

Вызывается все это дело следующим образом:


<?php

require_once __DIR__ . '/../vendor/autoload.php';

function m_print(string ...$messages): void
{
    foreach ($messages as $message) {
        print $message;
        print PHP_EOL;
    }
    print PHP_EOL;
    print '++++++++++++++++++++++++++++++++' . PHP_EOL;
}

(function()
{
    try {

        $user = ApiApi::users()->getUser(1);

        m_print($user->toJson(JSON_PRETTY_PRINT));

        m_print('Address', $user->address->toJson(JSON_PRETTY_PRINT));

        m_print('Address.Geo', $user->address->geo->toJson(JSON_PRETTY_PRINT));

        m_print('Company', $user->company->toJson(JSON_PRETTY_PRINT));
    }
    catch (Exception $e)
    {
        if(404 === $e->getCode()) print($e->getMessage());
    }

})();

Результат:


$ php src/index.php
{
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
        "street": "Kulas Light",
        "suite": "Apt. 556",
        "city": "Gwenborough",
        "zipcode": "92998-3874",
        "geo": {
            "lat": -37.3159,
            "lng": 81.1496
        }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
        "name": "Romaguera-Crona",
        "catchPhrase": "Multi-layered client-server neural-net",
        "bs": "harness real-time e-markets"
    }
}

++++++++++++++++++++++++++++++++
Address
{
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
        "lat": -37.3159,
        "lng": 81.1496
    }
}

++++++++++++++++++++++++++++++++
Address.Geo
{
    "lat": -37.3159,
    "lng": 81.1496
}

++++++++++++++++++++++++++++++++
Company
{
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
}

++++++++++++++++++++++++++++++++

Таким образом, с помощью библиотеки square/pjson можно сделать автоматическое преобразование JSONPHP Objects,
при этом заметьте, что в Api реализованы только GET-методы, тогда как там же можно добавить еще методы
POST, DELETE и другие.

Источник