PHP code example of polunich / wp-jsonapi

1. Go to this page and download the library: Download polunich/wp-jsonapi library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

polunich / wp-jsonapi example snippets


namespace Acme\Petstore;

use DateTimeImmutable;
use Polunich\WpJsonApi\Access\AllowAny;
use Polunich\WpJsonApi\ApiBuilder;
use Polunich\WpJsonApi\Codec\DateTimeCodec;
use Polunich\WpJsonApi\Codec\DigitsCodec;
use Polunich\WpJsonApi\Codec\StringCodec;
use Polunich\WpJsonApi\Declaration\Attribute;
use Polunich\WpJsonApi\Declaration\Implementation;
use Polunich\WpJsonApi\Declaration\ResourceType;
use Polunich\WpJsonApi\Declaration\ToOne;
use Polunich\WpJsonApi\JsonApi;
use Polunich\WpJsonApi\Operation\OperationKind;

final class PetstoreApi
{
    public static function define(): ApiBuilder
    {
        $anyone = Implementation::instance(new AllowAny());
        $pets = Implementation::instance(new PetStore());

        return JsonApi::api(name: 'petstore', version: '1.0.0', restNamespace: 'acme/v1')
            ->resource(
                ResourceType::make('pets')
                    ->id(new DigitsCodec(), static function (Pet $pet): string {
                        return $pet->id;
                    })
                    ->filterId('@eq')
                    ->attribute(
                        Attribute::make('name', new StringCodec(1, 200))
                            ->accessor(static function (Pet $pet): string {
                                return $pet->name;
                            })
                            ->sortable()
                            ->filter('@eq', '@containsi'),
                    )
                    ->attribute(
                        Attribute::make('listedAt', new DateTimeCodec())
                            ->accessor(static function (Pet $pet): DateTimeImmutable {
                                return $pet->listedAt;
                            })
                            ->sortable()
                            ->filter('@gte', '@lt'),
                    )
                    ->relationship(
                        ToOne::make('owner', 'customers')
                            ->relatedId(static function (Pet $pet): ?string {
                                return $pet->ownerId;
                            }),
                    )
                    ->reader($pets)
                    ->loader($pets)
                    ->permission(OperationKind::FetchCollection, $anyone)
                    ->permission(OperationKind::FetchResource, $anyone)
                    ->permission(OperationKind::FetchRelated, $anyone)
                    ->permission(OperationKind::FetchRelationship, $anyone)
                    ->defaultSort('-listedAt'),
            )
            ->resource(
                ResourceType::make('customers')
                    ->id(new DigitsCodec(), static function (Customer $customer): string {
                        return $customer->id;
                    })
                    ->attribute(
                        Attribute::make('name', new StringCodec())
                            ->accessor(static function (Customer $customer): string {
                                return $customer->name;
                            }),
                    )
                    ->loader(Implementation::instance(new CustomerStore()))
                    ->permission(OperationKind::FetchResource, $anyone),
            );
    }
}

namespace Acme\Petstore;

use DateTimeImmutable;

final readonly class Pet
{
    public function __construct(
        public string $id,
        public string $name,
        public DateTimeImmutable $listedAt,
        public ?string $ownerId,
    ) {}
}

final readonly class Customer
{
    public function __construct(public string $id, public string $name) {}
}

namespace Acme\Petstore;

use Closure;
use DateTimeImmutable;
use DateTimeZone;
use Polunich\WpJsonApi\Store\CollectionQuery;
use Polunich\WpJsonApi\Store\CollectionReader;
use Polunich\WpJsonApi\Store\LoadRequest;
use Polunich\WpJsonApi\Store\ResourceLoader;
use Polunich\WpJsonApi\Store\Slice;
use WP_Post;

/**
 * @implements CollectionReader<Pet>
 * @implements ResourceLoader<Pet>
 */
final class PetStore implements CollectionReader, ResourceLoader
{
    // `name` compares by its bytes, in the sort and in the filter alike,
    // so the conditions of a cursor agree with the order of the page
    public const COLUMNS = ['id' => 'ID', 'name' => 'CAST(post_title AS BINARY)', 'listedAt' => 'post_date_gmt'];

    public function read(CollectionQuery $query): Slice
    {
        global $wpdb;

        $where = "post_type = 'pet' AND post_status = 'publish'";

        if ($query->filter !== null) {
            $where .= ' AND ' . $query->filter->accept(new PetFilter());
        }

        $order = [];

        foreach ($query->sort->fields() as $field) {
            $order[] = self::COLUMNS[$field->field->toString()] . ' ' . $field->direction->value;
        }

        $ids = $wpdb->get_col(sprintf(
            "SELECT ID FROM {$wpdb->posts} WHERE %s ORDER BY %s LIMIT %d OFFSET %d",
            $where,
            implode(', ', $order),
            $query->window->limit,
            $query->window->offset,
        ));
        $total = $query->totalRequested
            ? (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->posts} WHERE {$where}")
            : null;

        return new Slice(array_values(array_filter(array_map($this->posts($ids), $ids))), $total);
    }

    public function load(LoadRequest $request): array
    {
        return array_map($this->posts($request->ids), $request->ids);
    }

    /**
     * @param list<string> $ids
     *
     * @return Closure(string): ?Pet
     */
    private function posts(array $ids): Closure
    {
        $pets = [];

        if ($ids !== []) {
            $posts = get_posts(['post_type' => 'pet', 'post__in' => array_map('intval', $ids), 'numberposts' => -1]);

            foreach ($posts as $post) {
                $pets[(string) $post->ID] = self::fromPost($post);
            }
        }

        return static function (string $id) use ($pets): ?Pet {
            return $pets[$id] ?? null;
        };
    }

    public static function fromPost(WP_Post $post): Pet
    {
        $ownerId = get_post_meta($post->ID, 'owner_id', true);

        return new Pet(
            (string) $post->ID,
            $post->post_title,
            new DateTimeImmutable($post->post_date_gmt, new DateTimeZone('UTC')),
            is_string($ownerId) && $ownerId !== '' ? $ownerId : null,
        );
    }
}

/**
 * @implements ResourceLoader<Customer>
 */
final class CustomerStore implements ResourceLoader
{
    public function load(LoadRequest $request): array
    {
        $customers = [];
        $posts = get_posts(['post_type' => 'customer', 'post__in' => array_map('intval', $request->ids), 'numberposts' => -1]);

        foreach ($posts as $post) {
            $customers[(string) $post->ID] = new Customer((string) $post->ID, $post->post_title);
        }

        return array_map(static function (string $id) use ($customers): ?Customer {
            return $customers[$id] ?? null;
        }, $request->ids);
    }
}

namespace Acme\Petstore;

use DateTimeImmutable;
use DateTimeZone;
use LogicException;
use Polunich\WpJsonApi\Query\Filter\AllOf;
use Polunich\WpJsonApi\Query\Filter\AnyOf;
use Polunich\WpJsonApi\Query\Filter\Condition;
use Polunich\WpJsonApi\Query\Filter\FilterNode;
use Polunich\WpJsonApi\Query\Filter\Not;
use Polunich\WpJsonApi\Query\Filter\RelationshipCondition;
use Polunich\WpJsonApi\Query\Filter\Visitor;

/**
 * @implements Visitor<string>
 */
final readonly class PetFilter implements Visitor
{
    // the operators the declaration opens, and `@gt` and `@lt` of a cursor
    private const COMPARISONS = ['@eq' => '=', '@gt' => '>', '@gte' => '>=', '@lt' => '<'];

    public function condition(Condition $node): string
    {
        global $wpdb;

        $value = $node->value instanceof DateTimeImmutable
            ? $node->value->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s')
            : $node->value;

        if ($node->operator === '@containsi') {
            return $wpdb->prepare('LOWER(post_title) LIKE LOWER(%s)', '%' . $wpdb->esc_like($value) . '%');
        }

        $column = PetStore::COLUMNS[$node->field->toString()];

        return $wpdb->prepare($column . ' ' . self::COMPARISONS[$node->operator] . ' %s', $value);
    }

    public function allOf(AllOf $node): string
    {
        return '(' . implode(' AND ', $this->each($node->nodes)) . ')';
    }

    public function anyOf(AnyOf $node): string
    {
        return '(' . implode(' OR ', $this->each($node->nodes)) . ')';
    }

    public function not(Not $node): string
    {
        return 'NOT (' . $node->node->accept($this) . ')';
    }

    public function relationshipCondition(RelationshipCondition $node): string
    {
        throw new LogicException('no attribute of a customer opens a filter, so none runs through the owner.');
    }

    /**
     * @param list<FilterNode> $nodes
     *
     * @return list<string>
     */
    private function each(array $nodes): array
    {
        return array_map(function (FilterNode $node): string {
            return $node->accept($this);
        }, $nodes);
    }
}

use Acme\Petstore\PetstoreApi;
use Polunich\WpJsonApi\Api;
use Polunich\WpJsonApi\JsonApi;

JsonApi::register(static function (): Api {
    return PetstoreApi::define()->build();
});

$builder = JsonApi::api(name: 'petstore', version: '1.0.0', restNamespace: 'acme/v1');

use Polunich\WpJsonApi\Query\NamingPolicy;

$builder->namingPolicy(NamingPolicy::CamelCase);

use Polunich\WpJsonApi\Declaration\ToMany;

$store = Implementation::instance(new PetStore());

ResourceType::make('pets')
    ->id(new DigitsCodec(), static function (Pet $pet): string {
        return $pet->id;
    })
    ->filterId('@eq', '@in')
    ->attribute(
        Attribute::make('name', new StringCodec(1, 200))
            ->accessor(static function (Pet $pet): string {
                return $pet->name;
            })
            ->sortable(),
    )
    ->relationship(
        ToOne::make('owner', 'customers')
            ->relatedId(static function (Pet $pet): ?string {
                return $pet->ownerId;
            }),
    )
    ->relationship(ToMany::make('orders', 'orders'))
    ->reader($store)
    ->loader($store)
    ->relatedLoader($store)
    ->creator($store)
    ->permission(OperationKind::FetchCollection, $anyone)
    ->defaultSort('-listedAt');

ResourceType::make('orders')
    // ...
    ->except(OperationKind::FetchCollection);

Attribute::make('listedAt', new DateTimeCodec())
    ->accessor(static function (Pet $pet): DateTimeImmutable {
        return $pet->listedAt;
    })
    ->creatable()
    ->updatable()
    ->

ResourceType::make('pets')
    ->queryParameter(OperationKind::FetchCollection, 'namePrefix', new StringCodec(1, 200))
    ->queryParameter(OperationKind::CreateResource, 'notifyOwner', new BooleanCodec(), 

use Polunich\WpJsonApi\Declaration\ResponseShape;
use Polunich\WpJsonApi\Declaration\BodyShape;

$builder->post('/pets/{id}/adopt', Implementation::instance(new AdoptPet()))
    ->name('adoptPet')
    ->bind('id', 'pets')
    ->body(BodyShape::toOne('customers'))
    ->responds(ResponseShape::record('id'))
    ->permission(Implementation::instance(new StaffOnly()))
    ->description('the customer the body names adopts the pet.');

namespace Acme\Petstore;

use Polunich\WpJsonApi\Endpoint\Response;
use Polunich\WpJsonApi\Endpoint\Handler;
use Polunich\WpJsonApi\Endpoint\Request;
use Polunich\WpJsonApi\Error\ErrorDescription;
use Polunich\WpJsonApi\Error\NotFoundException;
use Polunich\WpJsonApi\Operation\Linkage;

final class AdoptPet implements Handler
{
    public function handle(Request $request): Response
    {
        $pet = $request->record('id');
        assert($pet instanceof Pet && $request->body instanceof Linkage);

        // null where the body clears the owner
        $customer = $request->body->identifier();
        $post = $customer === null ? null : get_post((int) $customer->id);

        if ($customer !== null && ($post?->post_type !== 'customer' || $post->post_status !== 'publish')) {
            throw new NotFoundException([new ErrorDescription('customer_not_found', pointer: '/data')]);
        }

        update_post_meta((int) $pet->id, 'owner_id', $customer?->id ?? '');

        return Response::resource(new Pet($pet->id, $pet->name, $pet->listedAt, $customer?->id));
    }
}

$builder->get('/customers/{id}/pets', Implementation::instance(new CustomerPets()))
    ->name('customerPets')
    ->bind('id', 'customers')
    ->responds(ResponseShape::page('pets'))
    ->defaultSort('-listedAt')
    ->pagination(10, 50)
    ->permission(Implementation::instance(new AllowAny()));

final class CustomerPets implements Handler
{
    public function __construct(private PetReader $pets) {}

    public function handle(Request $request): Response
    {
        $query = $request->collectionQuery;
        assert($query !== null);

        return Response::page($this->pets->readOwnedBy($request->path['id'], $query));
    }
}

use Polunich\WpJsonApi\Codec\ObjectCodec;

$builder->get('/pets/export', Implementation::instance(new ExportPets()))
    ->name('exportPets')
    ->responds(ResponseShape::content('text/csv'))
    ->responds(ResponseShape::collection('pets'))
    ->permission(Implementation::instance(new AllowAny()));

$builder->post('/pets/{id}/photos', Implementation::instance(new AddPetPhoto()))
    ->name('addPetPhoto')
    ->bind('id', 'pets')
    ->body(BodyShape::form('multipart/form-data')
        ->field('caption', new StringCodec(), llowAny()));

namespace Acme\Petstore;

use Polunich\WpJsonApi\Endpoint\Response;
use Polunich\WpJsonApi\Endpoint\Handler;
use Polunich\WpJsonApi\Endpoint\Request;
use Polunich\WpJsonApi\Http\Content;

final class ExportPets implements Handler
{
    public function handle(Request $request): Response
    {
        $pets = array_map(PetStore::fromPost(...), get_posts(['post_type' => 'pet', 'numberposts' => -1]));

        if ($request->mediaType !== 'text/csv') {
            return Response::collection($pets);
        }

        return Response::content(Content::chunks(self::rows($pets)), 'text/csv', 'pets.csv');
    }

    /**
     * @param list<Pet> $pets
     *
     * @return iterable<string>
     */
    private static function rows(array $pets): iterable
    {
        yield "id,name,listedAt\r\n";

        foreach ($pets as $pet) {
            $name = '"' . str_replace('"', '""', $pet->name) . '"';

            yield $pet->id . ',' . $name . ',' . $pet->listedAt->format(DATE_RFC3339) . "\r\n";
        }
    }
}

use Polunich\WpJsonApi\Codec\ListCodec;
use Polunich\WpJsonApi\Codec\NumberCodec;
use Polunich\WpJsonApi\Declaration\Operator;

$builder->operator(Operator::make('@near', new ListCodec(new NumberCodec(), 2, 2)));

use Polunich\WpJsonApi\Authentication\SecurityScheme;

$paymentSignature = new SecurityScheme('paymentSignature', [
    'type' => 'apiKey',
    'in' => 'header',
    'name' => 'X-Payment-Signature',
]);

$builder->post('/orders/{id}/payment', Implementation::instance(new RecordPayment()))
    ->securitySchemes($paymentSignature);

ResourceType::make('pets')
    ->securitySchemes(OperationKind::DeleteResource, SecurityScheme::applicationPasswords());

ToMany::make('orders', 'orders')
    ->securitySchemes(OperationKind::FetchRelated, SecurityScheme::restNonce());

$builder->atomic('/operations')
    ->securitySchemes(SecurityScheme::applicationPasswords());

public function read(CollectionQuery $query): Slice;

$where = $query->filter?->accept(new MyVisitor());

public function load(LoadRequest $request): array;

public function loadRelated(RelatedLoadRequest $request): array;

$builder->transactionBoundary(Implementation::service('transactions'));

$builder->atomic('/operations')
    ->name('atomicOperations');

$builder->atomic('/pets/operations')
    ->name('petOperations')
    ->types('pets');

public function begin(): void;
public function commit(): void;
public function rollBack(): void;

interface OperationPermission
{
    public function allows(OperationAttempt $attempt): bool;
}

namespace Acme\Petstore;

use Polunich\WpJsonApi\Access\EndpointAttempt;
use Polunich\WpJsonApi\Access\EndpointPermission;

final class StaffOnly implements EndpointPermission
{
    public function allows(EndpointAttempt $attempt): bool
    {
        return current_user_can('edit_others_posts');
    }
}

public function challenges(ServerRequest $request, array $schemes): array;

use Polunich\WpJsonApi\Error\ErrorDescription;
use Polunich\WpJsonApi\Error\NotFoundException;

throw new NotFoundException([new ErrorDescription('pet_not_listed', detail: 'the pet is no longer listed.')]);

use Polunich\WpJsonApi\Error\ConflictException;
use Polunich\WpJsonApi\Error\ErrorDescription;
use Polunich\WpJsonApi\Error\ExceptionMapper;
use Polunich\WpJsonApi\Error\JsonApiException;
use Throwable;

/** @implements ExceptionMapper<PetAlreadySold> */
final class PetAlreadySoldMapper implements ExceptionMapper
{
    public function exceptionClass(): string
    {
        return PetAlreadySold::class;
    }

    public function map(Throwable $exception): JsonApiException
    {
        return new ConflictException([new ErrorDescription('pet_already_sold')], $exception);
    }
}

interface ErrorCatalog
{
    public function title(string $code): ?string;

    public function detail(string $code, array $parameters): ?string;
}

use Polunich\WpJsonApi\Declaration\LanguageSource;

$builder->languageSources(LanguageSource::queryParameter('uiLocale'), LanguageSource::acceptLanguage());

$builder
    ->strictQueryParameterNames(false)
    ->languageSources(LanguageSource::queryParameter('locale'), LanguageSource::acceptLanguage());

use Polunich\WpJsonApi\Testing\JsonApiAssertions;

/**
 * keeps the status and the header fields `serve_request()` sends,
 * which a command line process never sends
 */
final class RecordingServer extends WP_REST_Server
{
    public int $status = 200;

    /** @var array<string, string> */
    public array $headers = [];

    public function send_header($key, $value)
    {
        $this->headers[strtolower($key)] = $value;
    }

    public function remove_header($key)
    {
        unset($this->headers[strtolower($key)]);
    }

    protected function set_status($code)
    {
        $this->status = $code;
    }
}

final class PetsTest extends WP_UnitTestCase
{
    use JsonApiAssertions;

    public function testTheCollection(): void
    {
        add_filter('wp_rest_server_class', static function (): string {
            return RecordingServer::class;
        });
        $GLOBALS['wp_rest_server'] = null;
        $_SERVER['REQUEST_METHOD'] = 'GET';
        $_SERVER['HTTP_ACCEPT'] = 'application/vnd.api+json';
        $_GET = [];

        $server = rest_get_server();
        ob_start();
        $server->serve_request('/acme/v1/pets');
        $body = (string) ob_get_clean();

        self::assertResponseMatchesContract(
            __DIR__ . '/../openapi.json',
            '/pets',
            'GET',
            $server->status,
            $server->headers['content-type'] ?? null,
            $body,
        );
    }
}