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();
});
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));
}
}
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));
}
}
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)));
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());
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,
);
}
}
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.