View source
<?php
namespace Drupal\restful\Plugin\formatter;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\restful\Exception\BadRequestException;
use Drupal\restful\Exception\InternalServerErrorException;
use Drupal\restful\Exception\ServerConfigurationException;
use Drupal\restful\Exception\InaccessibleRecordException;
use Drupal\restful\Http\Request;
use Drupal\restful\Http\RequestInterface;
use Drupal\restful\Plugin\resource\DataInterpreter\DataInterpreterInterface;
use Drupal\restful\Plugin\resource\Field\ResourceFieldBase;
use Drupal\restful\Plugin\resource\Field\ResourceFieldCollectionInterface;
use Drupal\restful\Plugin\resource\Field\ResourceFieldInterface;
use Drupal\restful\Plugin\resource\Field\ResourceFieldResourceInterface;
use Drupal\restful\Plugin\resource\ResourceInterface;
class FormatterJsonApi extends Formatter implements FormatterInterface {
protected $contentType = 'application/vnd.api+json; charset=utf-8';
public function prepare(array $data) {
if (!empty($data['status']) && floor($data['status'] / 100) != 2) {
$this->contentType = 'application/problem+json; charset=utf-8';
return $data;
}
$extracted = $this
->extractFieldValues($data);
$included = array();
$output = array(
'data' => $this
->renormalize($extracted, $included),
);
$output = $this
->populateIncludes($output, $included);
if ($resource = $this
->getResource()) {
$request = $resource
->getRequest();
$data_provider = $resource
->getDataProvider();
$is_list_request = $request
->isListRequest($resource
->getPath());
if ($is_list_request) {
$output['meta']['count'] = $data_provider
->count();
if (variable_get('restful_show_access_denied', FALSE) && ($inaccessible_records = $data_provider
->getMetadata()
->get('inaccessible_records'))) {
$output['meta']['denied'] = empty($output['meta']['denied']) ? $inaccessible_records : $output['meta']['denied'] + $inaccessible_records;
}
}
else {
$output['data'] = reset($output['data']);
}
if (method_exists($resource, 'additionalHateoas')) {
$output = array_merge($output, $resource
->additionalHateoas($output));
}
$this
->addHateoas($output);
}
return $output;
}
protected function extractFieldValues($data, array $parents = array(), array $parent_hashes = array()) {
$output = array();
if ($this
->isCacheEnabled($data)) {
$parent_hashes[] = $this
->getCacheHash($data);
if ($cache = $this
->getCachedData($data)) {
return $cache->data;
}
}
foreach ($data as $public_field_name => $resource_field) {
if (!$resource_field instanceof ResourceFieldInterface) {
$parents[] = $public_field_name;
$output[$public_field_name] = static::isIterable($resource_field) ? $this
->extractFieldValues($resource_field, $parents, $parent_hashes) : $resource_field;
continue;
}
if (!$data instanceof ResourceFieldCollectionInterface) {
throw new InternalServerErrorException('Inconsistent output.');
}
$limit_fields = $data
->getLimitFields();
$output['#fields'] = empty($output['#fields']) ? array() : $output['#fields'];
if (!$this
->isCacheEnabled($data) && $limit_fields && !in_array($resource_field
->getPublicName(), $limit_fields)) {
continue;
}
$interpreter = $data
->getInterpreter();
if (!($id_field = $data
->getIdField())) {
throw new ServerConfigurationException('Invalid required ID field for JSON API formatter.');
}
$output['#fields'][$public_field_name] = $this
->embedField($resource_field, $id_field
->render($interpreter), $interpreter, $parents, $parent_hashes);
}
if ($data instanceof ResourceFieldCollectionInterface) {
$output['#resource_name'] = $data
->getResourceName();
$output['#resource_plugin'] = $data
->getResourceId();
$resource_id = $data
->getIdField()
->render($data
->getInterpreter());
if (!is_array($resource_id)) {
$output['#resource_id'] = (string) $resource_id;
try {
$output['#links']['self'] = restful()
->getResourceManager()
->getPlugin($output['#resource_plugin'])
->versionedUrl($output['#resource_id']);
} catch (PluginNotFoundException $e) {
}
}
}
if ($this
->isCacheEnabled($data)) {
$this
->setCachedData($data, $output, $parent_hashes);
}
return $output;
}
protected function addHateoas(array &$data, ResourceInterface $resource = NULL, $path = NULL) {
$top_level = empty($resource);
$resource = $resource ?: $this
->getResource();
$path = isset($path) ? $path : $resource
->getPath();
if (!$resource) {
return;
}
$request = $resource
->getRequest();
if (!isset($data['links'])) {
$data['links'] = array();
}
$input = $original_input = $request
->getParsedInput();
unset($input['page']);
unset($input['range']);
unset($original_input['page']);
unset($original_input['range']);
$input['page'] = $request
->getPagerInput();
$original_input['page'] = $request
->getPagerInput();
$options = $top_level ? array(
'query' => $input,
) : array();
$data['links']['self'] = $resource
->versionedUrl($path, $options);
$page = $input['page']['number'];
$items_per_page = $this
->calculateItemsPerPage($resource);
if (isset($data['meta']['count']) && $data['meta']['count'] > $items_per_page) {
$num_pages = ceil($data['meta']['count'] / $items_per_page);
unset($input['page']['number']);
$data['links']['first'] = $resource
->getUrl(array(
'query' => $input,
), FALSE);
if ($page > 1) {
$input = $original_input;
$input['page']['number'] = $page - 1;
$data['links']['previous'] = $resource
->getUrl(array(
'query' => $input,
), FALSE);
}
if ($num_pages > 1) {
$input = $original_input;
$input['page']['number'] = $num_pages;
$data['links']['last'] = $resource
->getUrl(array(
'query' => $input,
), FALSE);
if ($page != $num_pages) {
$input = $original_input;
$input['page']['number'] = $page + 1;
$data['links']['next'] = $resource
->getUrl(array(
'query' => $input,
), FALSE);
}
}
}
}
public function render(array $structured_data) {
return drupal_json_encode($structured_data);
}
public function getContentTypeHeader() {
return $this->contentType;
}
protected function getRequest() {
if ($resource = $this
->getResource()) {
return $resource
->getRequest();
}
return restful()
->getRequest();
}
protected function renormalize(array $output, array &$included, $allowed_fields = NULL, $includes_parents = array()) {
static $depth = -1;
$depth++;
if (!isset($allowed_fields)) {
$request = ($resource = $this
->getResource()) ? $resource
->getRequest() : restful()
->getRequest();
$input = $request
->getParsedInput();
$allowed_fields = empty($input['fields']) ? FALSE : explode(',', $input['fields']);
}
if (!is_array($output)) {
$depth--;
return $output;
}
$result = array();
if (ResourceFieldBase::isArrayNumeric($output)) {
foreach ($output as $item) {
$result[] = $this
->renormalize($item, $included, $allowed_fields, $includes_parents);
}
$depth--;
return $result;
}
if (!empty($output['#resource_name'])) {
$result['type'] = $output['#resource_name'];
}
if (!empty($output['#resource_id'])) {
$result['id'] = $output['#resource_id'];
}
if (!isset($output['#fields'])) {
$depth--;
return $this
->renormalize($output, $included, $allowed_fields, $includes_parents);
}
foreach ($output['#fields'] as $field_name => $field_contents) {
if ($allowed_fields !== FALSE && !in_array($field_name, $allowed_fields)) {
continue;
}
if (empty($field_contents['#embedded'])) {
$result['attributes'][$field_name] = $field_contents;
}
else {
$rel = array();
$single_item = $field_contents['#cardinality'] == 1;
$relationship_links = empty($field_contents['#relationship_links']) ? NULL : $field_contents['#relationship_links'];
unset($field_contents['#embedded']);
unset($field_contents['#cardinality']);
unset($field_contents['#relationship_links']);
foreach ($field_contents as $field_item) {
$include_links = empty($field_item['#include_links']) ? NULL : $field_item['#include_links'];
unset($field_contents['#include_links']);
$field_path = $this
->buildIncludePath($includes_parents, $field_name);
$field_item = $this
->populateCachePlaceholder($field_item, $field_path);
unset($field_item['#cache_placeholder']);
$element = $field_item['#relationship_info'];
unset($field_item['#relationship_info']);
$include_key = $field_item['#resource_plugin'] . '--' . $field_item['#resource_id'];
$nested_allowed_fields = $this
->unprefixInputOptions($allowed_fields, $field_name);
if (is_array($allowed_fields) && empty($nested_allowed_fields) && in_array($field_name, $allowed_fields)) {
$nested_allowed_fields = FALSE;
}
$new_includes_parents = $includes_parents;
$new_includes_parents[] = $field_name;
$included[$field_path][$include_key] = $this
->renormalize($field_item, $included, $nested_allowed_fields, $new_includes_parents);
$included[$field_path][$include_key] += $include_links ? array(
'links' => $include_links,
) : array();
$rel[$include_key] = $element;
}
$result['relationships'][$field_name] = array(
'data' => $single_item ? reset($rel) : array_values($rel),
);
if (!empty($relationship_links)) {
$result['relationships'][$field_name]['links'] = $relationship_links;
}
}
}
if (!empty($output['#links'])) {
$result['links'] = $output['#links'];
}
$depth--;
return $result;
}
protected function populateIncludes($output, $included) {
$input = $this
->getRequest()
->getParsedInput();
$requested_includes = empty($input['include']) ? array() : explode(',', $input['include']);
$included_keys = array();
foreach ($requested_includes as $requested_include) {
if (empty($included[$requested_include])) {
continue;
}
foreach ($included[$requested_include] as $include_key => $included_item) {
if (in_array($include_key, $included_keys)) {
continue;
}
$output['included'][] = $included_item;
$included_keys[] = $include_key;
}
}
return $output;
}
protected function embedField(ResourceFieldInterface $resource_field, $parent_id, DataInterpreterInterface $interpreter, array &$parents, array &$parent_hashes) {
static $embedded_resources = array();
if (!$resource_field instanceof ResourceFieldResourceInterface) {
return $resource_field
->render($interpreter);
}
$cardinality = $resource_field
->getCardinality();
$output = array();
$public_field_name = $resource_field
->getPublicName();
if (!($ids = $resource_field
->compoundDocumentId($interpreter))) {
return NULL;
}
$ids = $cardinality == 1 ? $ids = array(
$ids,
) : $ids;
$resource_info = $resource_field
->getResource();
$empty_value = array(
'#fields' => array(),
'#embedded' => TRUE,
'#resource_plugin' => sprintf('%s:%d.%d', $resource_info['name'], $resource_info['majorVersion'], $resource_info['minorVersion']),
'#cache_placeholder' => array(
'parents' => array_merge($parents, array(
$public_field_name,
)),
'parent_hashes' => $parent_hashes,
),
);
$value = array_map(function ($id) use ($empty_value) {
return $empty_value + array(
'#resource_id' => $id,
);
}, $ids);
if ($this
->needsIncluding($resource_field, $parents)) {
$cid = sprintf('%s:%d.%d--%s', $resource_info['name'], $resource_info['majorVersion'], $resource_info['minorVersion'], implode(',', $ids));
if (!isset($embedded_resources[$cid])) {
$result = $resource_field
->render($interpreter);
if (empty($result) || !static::isIterable($result)) {
$embedded_resources[$cid] = $result;
return $result;
}
$new_parents = $parents;
$new_parents[] = $public_field_name;
$result = $this
->extractFieldValues($result, $new_parents, $parent_hashes);
$embedded_resources[$cid] = $cardinality == 1 ? array(
$result,
) : $result;
}
$value = $embedded_resources[$cid];
}
$value = array_filter($value);
$resource_plugin = $resource_field
->getResourcePlugin();
foreach ($value as $value_item) {
$id = $value_item['#resource_id'];
$basic_info = array(
'type' => $resource_field
->getResourceMachineName(),
'id' => (string) $id,
);
$item = array(
'#resource_name' => $basic_info['type'],
'#resource_plugin' => $resource_plugin
->getPluginId(),
'#resource_id' => $basic_info['id'],
'#include_links' => array(
'self' => $resource_plugin
->versionedUrl($basic_info['id']),
),
'#relationship_info' => array(
'type' => $basic_info['type'],
'id' => $basic_info['id'],
),
) + $value_item;
$output[] = $item;
}
$links = array();
if ($resource = $this
->getResource()) {
$links['related'] = $resource
->versionedUrl('', array(
'absolute' => TRUE,
'query' => array(
'filter' => array(
$public_field_name => reset($ids),
),
),
));
$links['self'] = $resource_plugin
->versionedUrl($parent_id . '/relationships/' . $public_field_name);
}
return $output + array(
'#embedded' => TRUE,
'#cardinality' => $cardinality,
'#relationship_links' => $links,
);
}
protected function needsIncluding(ResourceFieldResourceInterface $resource_field, $parents) {
$input = $this
->getRequest()
->getParsedInput();
$includes = empty($input['include']) ? array() : explode(',', $input['include']);
return in_array($this
->buildIncludePath($parents, $resource_field
->getPublicName()), $includes);
}
protected function buildIncludePath(array $parents, $public_field_name = NULL) {
$array_path = $parents;
if ($public_field_name) {
array_push($array_path, $public_field_name);
}
$include_path = implode('.', array_filter($array_path, function ($item) {
return !is_numeric($item);
}));
return $include_path;
}
protected function populateCachePlaceholder(array $field_item, $includes_path) {
if (empty($field_item['#cache_placeholder']) || empty($field_item['#resource_id']) || empty($field_item['#resource_plugin'])) {
return $field_item;
}
$embedded_resource = restful()
->getResourceManager()
->getPluginCopy($field_item['#resource_plugin']);
$input = $this
->getRequest()
->getParsedInput();
$new_input = $input + array(
'include' => '',
'fields' => '',
);
$old_includes = array_filter(explode(',', $new_input['include']));
if (!in_array($includes_path, $old_includes)) {
return $field_item;
}
$new_input['fields'] = implode(',', $this
->unprefixInputOptions(explode(',', $new_input['fields']), $includes_path));
$new_input['include'] = implode(',', $this
->unprefixInputOptions($old_includes, $includes_path));
$embedded_resource
->setRequest(Request::create($this
->getRequest()
->getPath(), array_filter($new_input), $this
->getRequest()
->getMethod(), $this
->getRequest()
->getHeaders(), $this
->getRequest()
->isViaRouter(), $this
->getRequest()
->getCsrfToken(), $this
->getRequest()
->getCookies(), $this
->getRequest()
->getFiles(), $this
->getRequest()
->getServer(), $this
->getRequest()
->getParsedBody()));
try {
$data = $embedded_resource
->getDataProvider()
->view($field_item['#resource_id']);
} catch (InaccessibleRecordException $e) {
$data = array();
}
return array_merge($field_item, $this
->extractFieldValues($data, $field_item['#cache_placeholder']['parents'], $field_item['#cache_placeholder']['parent_hashes']));
}
public function parseBody($body) {
if (!($decoded_json = drupal_json_decode($body))) {
throw new BadRequestException(sprintf('Invalid JSON provided: %s.', $body));
}
if (empty($decoded_json['data'])) {
throw new BadRequestException(sprintf('Invalid JSON provided: %s.', $body));
}
$data = $decoded_json['data'];
$includes = empty($decoded_json['included']) ? array() : $decoded_json['included'];
$single_item = !ResourceFieldBase::isArrayNumeric($data);
$data = $single_item ? array(
$data,
) : $data;
$output = array();
foreach ($data as $item) {
$output[] = $this::restructureItem($item, $includes);
}
return $single_item ? reset($output) : $output;
}
protected static function restructureItem(array $item, array $included) {
if (empty($item['meta']['subrequest']) && empty($item['attributes']) && empty($item['relationship'])) {
throw new BadRequestException('Invalid JSON provided: both attributes and relationship are empty.');
}
$element = empty($item['attributes']) ? array() : $item['attributes'];
$relationships = empty($item['relationships']) ? array() : $item['relationships'];
foreach ($relationships as $field_name => $relationship) {
if (empty($relationship['data'])) {
throw new BadRequestException('Invalid JSON provided: relationship without data.');
}
$data = $relationship['data'];
$single_item = !ResourceFieldBase::isArrayNumeric($data);
$data = $single_item ? array(
$data,
) : $data;
$element[$field_name] = array();
foreach ($data as $info_pair) {
if (empty($info_pair['type'])) {
throw new BadRequestException('Invalid JSON provided: relationship item without type.');
}
if (empty($info_pair['id'])) {
throw new BadRequestException('Invalid JSON provided: relationship item without id.');
}
if (!empty($info_pair['meta']['subrequest']) && ($included_item = static::retrieveIncludedItem($info_pair['type'], $info_pair['id'], $included))) {
$value = array(
'body' => static::restructureItem($included_item, $included),
'id' => $info_pair['id'],
'request' => $info_pair['meta']['subrequest'],
);
if (!empty($value['request']['method']) && $value['request']['method'] == RequestInterface::METHOD_POST) {
unset($value['id']);
}
$element[$field_name][] = $value;
}
else {
$element[$field_name][] = array(
'id' => $info_pair['id'],
);
}
}
$element[$field_name] = $single_item ? reset($element[$field_name]) : $element[$field_name];
}
return $element;
}
protected static function retrieveIncludedItem($type, $id, array $included) {
foreach ($included as $item) {
if (!empty($item['type']) && $item['type'] == $type && !empty($item['id']) && $item['id'] == $id) {
return $item;
}
}
return NULL;
}
}