src/Prismic/Api.php
<?php
declare(strict_types=1);
namespace Prismic;
use Prismic\Exception;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Promise;
use GuzzleHttp\Exception\GuzzleException;
use Prismic\Cache\CacheInterface;
use Prismic\Cache\ApcCache;
use Prismic\Cache\NoCache;
use stdClass;
/**
* This class embodies a connection to your Prismic repository's API.
* Initialize it with Prismic::Api::get(), and use your Prismic::Api::form() to make API calls
* (read more in <a href="https://github.com/prismicio-community/php-kit">the kit's README file</a>)
*/
class Api
{
/**
* Kit version number
*/
const VERSION = "5.0.0";
/**
* Name of the cookie that will be used to remember the preview reference
*/
const PREVIEW_COOKIE = "io.prismic.preview";
/**
* Name of the cookie that will be used to remember the experiment reference
*/
const EXPERIMENTS_COOKIE = "io.prismic.experiment";
/**
* The API's access token to be used with each API call
* @var string|null
*/
protected $accessToken;
/**
* An instance of ApiData containing information about types, tags and refs etc
* @var ApiData
*/
protected $data;
/**
* The cache instance
* @var CacheInterface
*/
private $cache;
/**
* Guzzle HTTP Client
* @var ClientInterface
*/
private $httpClient;
private function __construct(
ApiData $data,
?string $accessToken = null,
?ClientInterface $httpClient = null,
?CacheInterface $cache = null
) {
$this->data = $data;
$this->accessToken = $accessToken;
$this->httpClient = is_null($httpClient) ? new Client() : $httpClient;
$this->cache = is_null($cache) ? static::defaultCache() : $cache;
}
/**
* This is the factory method with which to retrieve your API client instance
*
* If your API is set to "public" or "open", you can instantiate your Api object just like this:
* Api::get('https://your-repository-name.prismic.io/api/v2')
*
* @param string $action The URL of your repository API's endpoint
* @param string $accessToken A permanent access token to use if your repository API is set to private
* @param ClientInterface $httpClient Custom Guzzle http client
* @param CacheInterface $cache Cache implementation
* @param int $apiCacheTTL Max time to keep the API object in cache (in seconds)
* @return static
*/
public static function get(
string $action,
?string $accessToken = null,
?ClientInterface $httpClient = null,
?CacheInterface $cache = null,
int $apiCacheTTL = 5
) : self {
$cache = is_null($cache) ? static::defaultCache() : $cache;
$cacheKey = $action . (empty($accessToken) ? "" : ("#" . $accessToken));
$apiData = $cache->get($cacheKey);
if (is_string($apiData) && ! empty($apiData)) {
return new static(unserialize($apiData), $accessToken, $httpClient, $cache);
}
$url = $accessToken ? Utils::buildUrl($action, [ 'access_token' => $accessToken]) : $action;
$httpClient = is_null($httpClient) ? new Client() : $httpClient;
try {
/** @var \Psr\Http\Message\ResponseInterface $response */
$response = $httpClient->request('GET', $url);
} catch (GuzzleException $guzzleException) {
throw Exception\RequestFailureException::fromGuzzleException($guzzleException);
}
$apiData = ApiData::withJsonString((string) $response->getBody());
$api = new static($apiData, $accessToken, $httpClient, $cache);
$cache->set($cacheKey, serialize($apiData), $apiCacheTTL);
return $api;
}
/**
* Returns all of the repository's references (queryable points in time)
*
* @return Ref[]
*/
public function refs() : array
{
$groupBy = [];
foreach ($this->data->getRefs() as $ref) {
$label = $ref->getLabel();
if (! isset($groupBy[$label])) {
$groupBy[$label] = $ref;
}
}
return $groupBy;
}
/**
* Return the ref identified by the given label
*
* @param string $label The label of the requested ref
*
* @return Ref|null a reference or null
*/
public function getRefFromLabel(string $label) :? Ref
{
$refs = $this->refs();
return $refs[$label];
}
/**
* Returns the list of all bookmarks on the repository. If you're looking
* for a document from it's bookmark name, you should use the bookmark() function.
*
* @return array the array of bookmarks
*/
public function bookmarks() : array
{
return $this->data->getBookmarks();
}
/**
* From a bookmark name, returns the ID of the attached document.
* You can then use this ID for anything, for instance to query with a predicate
* that looks like this [:d = at(document.id, "abcdefghijkl")].
* Most starter projects embed a helper to query a document from their ID string,
* which makes this even easier.
*
* @param string $name the bookmark name to use
*
* @return string|null the ID string for a given bookmark name
*/
public function bookmark(string $name) :? string
{
$bookmarks = $this->bookmarks();
if (isset($bookmarks[$name])) {
return $bookmarks[$name];
}
return null;
}
/**
* Returns the master ref repository: the ref which is to be used to query content
* that is live right now.
*
* @return Ref the master ref
*/
public function master() : Ref
{
$masters = array_filter($this->data->getRefs(), function (Ref $ref) {
return $ref->isMasterRef() == true;
});
return $masters[0];
}
/**
* Returns the form of type Prismic::SearchForm based on its name.
* The intended syntax of a call is: api->form('everything')->query(query)->ref(ref)->submit().
* Learn more about those keywords in Prismic's documentation on our developers' portal.
*/
public function form(string $formName) : SearchForm
{
$forms = $this->data->getForms();
$formObject = Form::withJsonObject($forms[$formName]);
$data = $formObject->defaultData();
return new SearchForm($this->httpClient, $this->cache, $formObject, $data);
}
public function getExperiments() : Experiments
{
return $this->data->getExperiments();
}
/**
* Return the URL to display a given preview
* @param string $token as received from Prismic server to identify the content to preview
* @param LinkResolver $linkResolver the link resolver to build URL for your site
* @param string $defaultUrl the URL to default to return if the preview doesn't correspond to a document
* (usually the home page of your site)
* @return string the URL you should redirect the user to preview the requested change
*/
public function previewSession(string $token, LinkResolver $linkResolver, string $defaultUrl) : string
{
try {
$response = $this->getHttpClient()->request('GET', $token);
} catch (GuzzleException $guzzleException) {
throw Exception\RequestFailureException::fromGuzzleException($guzzleException);
}
/** @var \Psr\Http\Message\ResponseInterface $response */
$response = \json_decode((string) $response->getBody());
if (isset($response->mainDocument)) {
$documents = $this
->query(Predicates::at("document.id", $response->mainDocument), ['ref' => $token, 'lang' => '*'])
->results;
if (count($documents) > 0) {
if ($url = $linkResolver($documents[0])) {
return $url;
}
}
}
return $defaultUrl;
}
/**
* Return the URL of the endpoint to initiate OAuth authentication.
*/
public function oauthInitiateEndpoint() : string
{
return $this->data->getOauthInitiate();
}
/**
* Return the URL of the endpoint to use OAuth authentication.
*/
public function oauthTokenEndpoint() : string
{
return $this->data->getOauthToken();
}
/**
* Accessing raw data returned by the /api endpoint
*/
public function getData() : ApiData
{
return $this->data;
}
/**
* Accessing the cache object specifying how to store the cache
*/
public function getCache() : CacheInterface
{
return $this->cache;
}
/**
* Accessing the underlying Guzzle HTTP client
*/
public function getHttpClient() : ClientInterface
{
return $this->httpClient;
}
/**
* Submit several requests in parallel
*
* @TODO Discover the use-case for this method and either refactor it or remove it
* @return array
*/
public function submit() : array
{
$numargs = func_num_args();
if ($numargs == 1 && is_array(func_get_arg(0))) {
$forms = func_get_arg(0);
} else {
$forms = func_get_args();
}
$responses = [];
// Get what we can from the cache
$all_urls = [];
$promises = [];
$urls = [];
foreach ($forms as $i => $form) {
$url = $form->url();
array_push($all_urls, $url);
$json = $this->getCache()->get($url);
if ($json) {
$responses[$i] = $json;
} else {
$responses[$i] = null;
array_push($urls, $url);
$promises[$url] = $this->getHttpClient()->getAsync($url);
}
}
// Query the server for the rest
if (count($promises) > 0) {
$raw_responses = Promise\Utils::unwrap($promises);
foreach ($urls as $url) {
$response = $raw_responses[$url];
$cacheControl = $response->getHeader('Cache-Control')[0];
$cacheDuration = null;
if (preg_match('/^max-age\s*=\s*(\d+)$/', $cacheControl, $groups) == 1) {
$cacheDuration = (int) $groups[1];
}
$json = json_decode($response->getBody(true));
if (! isset($json)) {
throw new Exception\RuntimeException("Unable to decode json response");
}
if ($cacheDuration !== null) {
$expiration = $cacheDuration;
$this->getCache()->set($url, $json, $expiration);
}
$idx = array_search($url, $all_urls);
$responses[$idx] = $json;
}
}
return $responses;
}
/**
* If a preview cookie is set, return the ref stored in that cookie
*/
private function getPreviewRef() :? string
{
$cookieNames = [
str_replace(['.',' '], '_', self::PREVIEW_COOKIE),
self::PREVIEW_COOKIE,
];
foreach ($cookieNames as $cookieName) {
if (isset($_COOKIE[$cookieName])) {
return $_COOKIE[$cookieName];
}
}
return null;
}
/**
* If an experiment cookie is set, return the ref as determined by \Prismic\Experiments::refFromCookie
*/
private function getExperimentRef() :? string
{
$cookieNames = [
str_replace(['.',' '], '_', self::EXPERIMENTS_COOKIE),
self::EXPERIMENTS_COOKIE,
];
foreach ($cookieNames as $cookieName) {
if (isset($_COOKIE[$cookieName])) {
$experiments = $this->getExperiments();
return $experiments->refFromCookie($_COOKIE[$cookieName]);
}
}
return null;
}
/**
* Whether the current ref in use is a preview, i.e. the user is in preview mode
*/
public function inPreview() : bool
{
return null !== $this->getPreviewRef();
}
/**
* Whether the current ref in use is an experiment
*/
public function inExperiment() : bool
{
return null !== $this->getExperimentRef() && false === $this->inPreview();
}
/**
* Return the ref currently in use
*
* In order of preference, returns the preview cookie, the experiments cookie or the master ref otherwise
*/
public function ref() : string
{
$preview = $this->getPreviewRef();
if ($preview) {
return $preview;
}
$experiment = $this->getExperimentRef();
if ($experiment) {
return $experiment;
}
return (string) $this->master()->getRef();
}
/**
* Shortcut to query on the default reference.
* Use the reference from previews or experiment cookie, fallback to the master reference otherwise.
*
* @param string|array|Predicate $q the query, as a string, predicate or array of predicates
* @param array $options query options: pageSize, orderings, etc.
* @return stdClass
*/
public function query($q, array $options = []) : stdClass
{
$ref = $this->ref();
/** @var SearchForm $form */
$form = $this->form('everything')->ref($ref);
if (! empty($q)) {
$form = $form->query($q);
}
foreach ($options as $key => $value) {
$form = $form->set($key, $value);
}
return $form->submit();
}
/**
* Return the first document matching the query
* Use the reference from previews or experiment cookie, fallback to the master reference otherwise.
*
* @param string|array|Predicate $q the query, as a string, predicate or array of predicates
* @param array $options query options: pageSize, orderings, etc.
*
* @return stdClass|null the resulting document, or null
*/
public function queryFirst($q, array $options = []) :? stdClass
{
$documents = $this->query($q, $options)->results;
if (count($documents) > 0) {
return $documents[0];
}
return null;
}
/**
* Search a document by its id
*
* @param string $id the requested id
* @param array $options query options: pageSize, orderings, etc.
*
* @return stdClass|null the resulting document (null if no match)
*/
public function getByID(string $id, array $options = []) :? stdClass
{
$options = $this->prepareDefaultQueryOptions($options);
return $this->queryFirst(Predicates::at("document.id", $id), $options);
}
/**
* Search a document by its uid
*
* @param string $type the custom type of the requested document
* @param string $uid the requested uid
* @param array $options query options: pageSize, orderings, etc.
* @return stdClass|null the resulting document (null if no match)
*/
public function getByUID(string $type, string $uid, array $options = []) :? stdClass
{
$options = $this->prepareDefaultQueryOptions($options);
return $this->queryFirst(Predicates::at("my.".$type.".uid", $uid), $options);
}
/**
* Return a set of document from their ids
*
* @param array $ids array of strings, the requested ids
* @param array $options query options: pageSize, orderings, etc.
*
* @return stdClass the response, including documents and pagination information
*/
public function getByIDs(array $ids, array $options = []) : stdClass
{
$options = $this->prepareDefaultQueryOptions($options);
return $this->query(Predicates::in("document.id", $ids), $options);
}
/**
* Get a single typed document by its type
*
* @param string $type the custom type of the requested document
* @param array $options query options: pageSize, orderings, etc.
*
* @return stdClass|null the resulting document (null if no match)
*/
public function getSingle(string $type, array $options = []) :? stdClass
{
return $this->queryFirst(Predicates::at("document.type", $type), $options);
}
/**
* Use the APCu cache if APCu is activated on the server, otherwise fallback to the noop cache (no cache)
*/
public static function defaultCache() : CacheInterface
{
if (extension_loaded('apcu') && ini_get('apc.enabled')) {
return new ApcCache();
}
return new NoCache();
}
/**
* Given an options array for a query, fill the lang parameter with a default value
* @param array $options
* @return array
*/
private function prepareDefaultQueryOptions(array $options) : array
{
if (! isset($options['lang'])) {
$options['lang'] = '*';
}
return $options;
}
}