Duplicate default WP REST API (v2) endpoints - wordpress

How can i duplicate default wp rest api V2 endpoints? I would like to keep default endpoints and routs intact, but would like to use simplified responses for my application.
Wordpress v4.7
My code for now:
function register_custom_routes()
{
$controller = new MY_REST_Posts_Controller;
$controller->register_routes();
}
add_action( 'rest_api_init', 'register_custom_routes', 1 );
class MY_REST_Posts_Controller extends WP_REST_Controller {
// this is a copy of default class WP_REST_Posts_Controller
}
Calling http://localhost/wp/wp-json/ list my namespace ( /myrest ), also
http://localhost/wp/wp-json/myrest/gives me:
{
"namespace": "myrest",
"routes": {
"/myrest": {
"namespace": "myrest",
"methods": [
"GET"
],
...
"/myrest/(?P<id>[\\d]+)": {
"namespace": "myrest",
"methods": [
"GET",
"POST",
"PUT",
"PATCH",
"DELETE"
],
...
}
but when i try to list posts with http://localhost/wp/wp-json/myrest/posts (like with default api route call) it does not work:
{
"code": "rest_no_route",
"message": "No route was found matching the URL and request method",
"data": {
"status": 404
}
}
I need simplified version of get posts response for android app but also want to keep default rest endpoints and routs as is.

Here is the solution. I've wrapped code in wp plugin.
class WP_REST_custom_controller extends WP_REST_Controller {
// this is a copy of default class WP_REST_Posts_Controller
// Edited constructor for cutom namespace and endpoint url
/**
* Constructor.
*
* #since 4.7.0
* #access public
*
* #param string $post_type Post type.
*/
public function __construct() {
$this->post_type = 'post';
$this->namespace = 'custom_namespace/v1';
$obj = get_post_type_object( $post_type );
$this->rest_base = ! empty( $obj->rest_base ) ? $obj->rest_base : $obj->name;
$this->resource_name = 'posts';
$this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
}
// this is a copy of default class WP_REST_Posts_Controller with necessary edits
}
// Function to register our new routes from the controller.
function register_custom_rest_routes() {
$controller = new WP_REST_custom_controller();
$controller->register_routes();
}
add_action( 'rest_api_init', 'register_custom_rest_routes');

Related

API Platform: How to normalize a collection of embedded entities in GraphQL?

I'm trying to make a collection of subresources selectable in GraphQL (with pagination). I'd like to be able to query:
query {
getA(id: '/api/A/1') {
aId
subresources {
totalCount
pageInfo {
endCursor
startCursor
hasNextPage
hasPreviousPage
}
edges {
node {
bId
}
}
}
}
}
and get the result:
{
aId: 1,
subresources: {
"totalCount": XX,
"pageInfo": {
"endCursor": "MQ==",
"startCursor": "MA==",
"hasNextPage": true,
"hasPreviousPage": false
},
edges: [
{
node: {
bId: 11
}
},
{
node: {
bId: 12
}
},
{
node: {
bId: 13
}
}
]
}
}
I'm not using Doctrine at all- I'm using custom data providers. The problem I'm encountering is that even when I return an A entity from DataProvider::getItem() that has an array of B subresources, I get an empty array for subresources in GraphQL. I get the correct data in REST though.
I'm following the instructions given in SymfonyCasts and I found a related API Platform issue, but I'm still having no luck.
I traced through API Platform core and I think it has to do with how the entity is normalized in GraphQL. Specifically, an empty array is returned in ItemNormalizer::normalizeCollectionOfRelations(). However, there's a comment saying "to-many are handled directly by the GraphQL resolver" but I'm not sure what that refers to.
Here's the entity code.
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Serializer\Annotation\Groups;
#[ApiResource(
graphql: ['item_query', 'collection_query', 'create', 'update', 'delete'],
collectionOperations: ['get', 'post'],
itemOperations: ['get', 'put', 'patch', 'delete'],
normalizationContext: ['groups' => ['read']],
denormalizationContext: ['groups' => ['write']],
)]
class A {
#[ApiProperty(identifier: true)]
#[Groups(['read', 'write'])]
public ?int $aId = null,
/** #var B[] */
#[ApiProperty(readableLink: true, writableLink: true)]
#[Groups(['read', 'write'])]
public $subresources = []
}
And:
#[ApiResource(
graphql: ['item_query', 'collection_query', 'create', 'update', 'delete'],
collectionOperations: ['get', 'post'],
itemOperations: ['get', 'put', 'patch', 'delete'],
normalizationContext: ['groups' => ['read']],
denormalizationContext: ['groups' => ['write']],
)]
class B {
#[ApiProperty(identifier: true)]
#[Groups(['read', 'write'])]
public ?int $bId = null,
}
My ADataProvider:
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []): A {
$bs = $this->bDataProvider->getCollection(B::class, null, []);
return new A(123, $bs);
}
My BDataProvider:
/**
* #return ArrayPaginator<B>
*/
public function getCollection(string $resourceClass, string $operationName = null, array $context = []): ArrayPaginator {
return ArrayPaginator::fromList([new B(11), new B(12), new B(13)]);
}
ArrayPaginator implements IteratorAggregate and PaginatorInterface.
Specifically I see this error:
{
"errors": [
{
"debugMessage": "Collection returned by the collection data provider must implement ApiPlatform\\Core\\DataProvider\\PaginatorInterface or ApiPlatform\\Core\\DataProvider\\PartialPaginatorInterface.",
"message": "Internal server error",
"extensions": {
"category": "internal"
},
"locations": [
{
"line": 29,
"column": 5
}
],
"path": [
"a",
"b"
],
"trace": [
{
"file": "/homedir/core/src/GraphQl/Resolver/Stage/SerializeStage.php",
"line": 100,
"call": "ApiPlatform\\Core\\GraphQl\\Resolver\\Stage\\SerializeStage::serializeCursorBasedPaginatedCollection(array(0), array(5), array(6))"
},
TLDR: How does one use annotations (or YAML) to make attributes that are collections of subresources selectable in GraphQL?
Any help/ideas are appreciated, thanks for reading!
Found a solution: the ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface needs to be implemented by the BDataProvider.
It gets used in the ReadStage of api platform's graphql resolver. Surprisingly, it's found nowhere in the REST resolver, so this won't get called on a REST request.
The only method that needs to be implemented is getSubresource(). My basic first implementation looks like this:
public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null) {
if ($context['collection']) {
return $this->getCollection($resourceClass, $operationName, $context);
}
$id = // get your id from $identifiers;
return $this->getItem($resourceClass, $id, $operationName, $context);
}
This isn't found in the docs unfortunately, but there are a few pulls (1, 2) open to add it.

api_platform produces Error "no handler found for uri [/index/_doc/_search] and method [POST]"

When trying to implement elasticsearch (v7.9.3) via the fos_elastica-bundle (v6.0.0) into my Symfony (v5.3.10) - App with api_platform (v2.6.6), I keep on getting this error:
"{"error":"no handler found for uri [//posts/_doc/_search] and method [POST]"}",
My api_platform.yaml reads:
api_platform:
[...]
elasticsearch:
hosts: [ '%env(ELASTICSEARCH_URL)%' ]
mapping:
App\Document\Post:
index: posts
and my fos_elastica.yaml:
fos_elastica:
clients:
default: { url: '%env(ELASTICSEARCH_URL)%' }
indexes:
posts:
properties:
id:
"type": "keyword"
source: ~
title: ~
description: ~
body: ~
children: ~
tags: ~
originalContent: ~
persistence:
driver: mongodb
model: App\Document\Post
By debugging the fos-elastica Bundle, I found out that the Elastica-Connector correctly triggers a [POST]-Request to "/posts/_doc/_search" with this request body:
{"sort":[{"id":{"order":"asc"}}],"query":{"match_all":{}},"size":30,"from":0}
If I use the Kibana Dev Tools Console and trigger an identical request
POST /posts/_doc/_search
{"sort":[{"id":{"order":"asc"}}],"query":{"match_all":{}},"size":30,"from":60}
I do get results from elasticsearch as expected:
#! Deprecation: [types removal] Specifying types in search requests is deprecated.
{
"took" : 12,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 3082,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "posts",
"_type" : "_doc",
[...]
Apart from the deprecation notice, everything seems fine.
Does anyone have an idea why the api_platform integration of the fos_elastica-bundle does not work as expected and keeps on returning the "no handler found"-error message?
I have now helped myself by creating a custom ApiResource - filter
#[ApiFilter(FulltextFilter::class, arguments: ['index' => 'post'], properties: ['body','description','tag'])]
My custom filter implements ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\FilterInterface, directly communicates with the ElasticSearch server, sends a query to search the specified index (posts) and adds another match()-directive to the aggregationBuilder with a set of IDs matching the original search:
<?php
declare(strict_types=1);
namespace App\Filter;
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Filter\FilterInterface;
use Doctrine\ODM\MongoDB\Aggregation\Builder;
use Elastica\Result;
use Elastica\Client;
use Elastica\Query;
use Symfony\Component\PropertyInfo\Type;
/**
* Filter the collection by given properties.
*
*/
final class FulltextFilter implements FilterInterface
{
protected $index = '';
protected $properties = [];
protected $client;
protected $searchParameterName;
protected $maxResultsParameterName;
const DEFAULT_MAX_RESULTS = 200;
public function __construct(Client $client, string $index = '', string $maxResultsParameterName = 'amount', string $searchParameterName = 'query', array $properties = []) {
$this->index = $index;
$this->properties = $properties;
$this->client = $client;
$this->searchParameterName = $searchParameterName;
$this->maxResultsParameterName = $maxResultsParameterName;
}
public function getFilteredIds($searchterm, $index = null, $properties = null, $maxResults = null) {
$matches = [];
if (is_null($properties)) {
$properties = array_keys($this->properties);
}
foreach ($properties as $propertyName) {
array_push($matches, ['match'=>[$propertyName => $searchterm]]);
}
$queryObject = ['query' => ['bool' => ['should' => $matches]]];
$queryObject['size'] = (int) $maxResults >0 ? (int) $maxResults : self::DEFAULT_MAX_RESULTS;
$query = new Query();
$response = $this->client->getIndex($index ?? $this->index)
->search($query->setRawQuery($queryObject))
->getResults();
return array_map(function(Result $result) {return $result->getHit()['_source']['id'];}, $response);
}
public function apply(Builder $aggregationBuilder, string $resourceClass, string $operationName = null, array &$context = [])
{
$maxResults = $context['filters'][$this->maxResultsParameterName] ?? null;
$searchterm = $context['filters'][$this->searchParameterName] ?? false;
if ($searchterm !== false) {
$aggregationBuilder->match()->field('id')->in($this->getFilteredIds($searchterm, null, null, $maxResults));
}
}
public function getDescription(string $resourceClass): array
{
return [];
}
}
This solution might not be as elegant as using the ElasticSearch-Connector natively provided by api_platform, but it is fairly performant and it works.
However, if someone comes up with a solution to fix the depicted ES-Connector issue with api_platform, please feel free to share it.
The problem is that, FOS Elastica requires an ES URL with an ending slash. But Api Platform requires a URL without ending slash.
We usually define the URL in .env file and then recall it in config files.
To solve the problem, we could define the URL in .env without endling slash and add the slash to the FOS Elastica config.
# .env
###> friendsofsymfony/elastica-bundle ###
ELASTICSEARCH_URL=http://localhost:9200
###< friendsofsymfony/elastica-bundle ###
# config/packages/api_platform.yaml
api_platform:
elasticsearch:
enabled: true
hosts: [ '%env(ELASTICSEARCH_URL)%' ]
# config/packages/fos_elastica.yaml
fos_elastica:
clients:
default: { url: '%env(ELASTICSEARCH_URL)%/' }

Symfony + FOSRestBundle - How to allow NULL value to a field configured with a custom form type?

I have a simple Symfony API which uses FOSRestBundle. I have an Exercise entity which contains a field sentences. This field is of type json #ORM\Column(type="json") and is stuffed with some nested json. The entity is persisted in a MySQL database.
I use Symfony forms to validate incoming data from a SPA. Here's the data the SPA sends on the endpoint /exercise:
{
"name": "HEP9H",
"sentences": [
{
"name": "Sentence",
"tirettes": [
{
"chain": null
},
{
"chain": {
"name": "Chain 1"
}
}
]
}
]
}
Once persisted, the API then returns the entity as JSON. It should look exactly the same, except that it has an ID. The issue is that I get this piece of JSON in return:
{
"id": 21,
"name": "HEP9H",
"sentences": [
{
"name": "Sentence",
"tirettes": [
{
"chain": {
"name": null
}
},
{
"chain": {
"name": "ChaƮne 1"
}
}
]
}
]
}
As you can see, the problem is that my property "chain": null becomes "chain": {"name": null}. I guess this is due to a bad form type configuration. The data structure changes right after I validate my form and before I persist the entity for the first time.
Here's TiretteType:
class TiretteType extends AbstractType {
public function buildForm ( FormBuilderInterface $builder, array $options ) {
$builder
->add ( 'chain', ChainType::class, [
"required" => false
] );
}
}
And here's ChainType:
class ChainType extends AbstractType {
public function buildForm ( FormBuilderInterface $builder, array $options ) {
$builder->add ( 'name', TextType::class );
}
}
I have no underlying data class and no underlying entity (except the root entity Exercise).
What I've tried so far:
adding "required" => false to the 'chain' field, it doesn't change anything
setting "empty_data" => NULL to the 'chain' field, this also doesn't work and overrides any data to NULL
Am I completely missing something?
Thanks!
I found the answer to my issue. Since my field chain had no underlying data class, the form would simply give me an array with default values if it had a null value as input.
The solution is to use a data transformer (https://symfony.com/doc/current/form/data_transformers.html). I had to check for such an empty structure and if found, return back null instead of the given value.
$builder->get ( 'chain' )->addModelTransformer ( new CallbackTransformer(
function ( $originalInput ) {
return $originalInput;
},
function ( $submittedValue ) {
return $submittedValue["name"] === null ? $submittedValue : null;
}
) );
I don't think checking for null properties is the cleanest way to do this but my case is very simple so I won't spend more time on this one.
Hope this helps someone.

class not found when unit testing a custom module

I'm trying to write tests for a custom module I've written on Drupal 8 and keep getting an error and at this point I'm out of ideas. Here is the error:
Error: Class 'Drupal\mypackage\Services\Config\MyClassServiceConfig' not found
The PhpUnit class is under
modules\custom\mypackage\tests\src\Unit\mypackageUserAuthTest
Here is the code
class mypackageUserAuthTest extends UnitTestCase
{
protected $user;
protected $loginService;
public function setUp()
{
parent::setUp();
$this->loginService = new LoginService();
$this->user = [
'username' => 'xxx',
'password' => 'xxx',
'deviceId' => 'xxx',
'some-token' => 'xxx'
];
}
/** #test */
public function that_we_can_authenticate_a_user()
{
$IsUserLoggedIn = $this->loginService->login($this->user['username'], $this->user['password']);
$this->assertTrue($IsUserLoggedIn);
}
Now the method login in loginService code
<?php
namespace Drupal\mypackage\Rest;
use GuzzleHttp\Exception\ClientException;
use Drupal\mypackage\Services\RestServiceFactory;
use Drupal\mypackage\Services\Config\MyClassServiceConfig;
class LoginService
{
public function login($username, $password)
{
$configs = new MyClassServiceConfig(null, "mobile", "v1");
$client = RestServiceFactory::create($configs);
try {
$response = $client->post('login', [
'json' => [
'username' => $username,
'password' => $password,
'deviceId' => 'onepiece',
],
]);
return json_decode($response->getBody(), true);
} catch (ClientException $exception) {
switch ($$exception->getResponse()->getStatusCode()) {
case 402: // This only applies to co members
throw new SubscriptionRequiredException();
case 403:
throw new BlockedAccountException();
case 409:
throw new DuplicateEmailException();
case 410:
throw new PasswordDoesNotExistException();
}
throw $exception;
}
}
}
pwd result on MyClassServiceConfig class directory
/var/www/cms/web/modules/custom/mypackage/src/Services/Config
But it seems to fail on the line $configs = new MyClassServiceConfig(null, "mobile", "v1"); with the previously mentioned error :
1) Drupal\Tests\mypackage\Unit\mypackageUserAuthTest::that_we_can_authenticate_a_user
Error: Class 'Drupal\mypackage\Services\Config\MyClassServiceConfig' not found
Btw, I'm using drupal-project structure (https://github.com/drupal-composer/drupal-project)
So I spent days checking the path but it seemed that the files were not loading so I ended up adding the custom module to autload-dev part composer.json.
"autoload": {
"classmap": [
"scripts/composer/ScriptHandler.php"
],
"files": ["load.environment.php"]
},
"autoload-dev": {
"psr-4": { "Drupal\\mypackage\\": "web/modules/custom/mypackage" }
},
Now at least it seems to load the module as I'm getting an other error related to Drupal Container
\Drupal::$container is not initialized yet. \Drupal::setContainer() must be called with a real container.
It is an old question, the same thing happened to me, as I managed to solve it in my case it was as follows:
In the comment of the class where the tests are carried out, something similar to this should go:
The #coversDefaultClass annotation must go with the namespace of the class to test.
/**
* #coversDefaultClass \Drupal\my_module\MyModuleClassName
* #group my_module
*/
class MyModuleCaseTest extends UnitTestCase {
}
Maybe it will serve someone else

Trying to retrieve file from request with Silex/Symfony2

I'm retrieving a POSTed file with:
$this->app->post(Extension::API_PREFIX . "profile/picture", array($this, 'post_profile_pic'))
->bind('post_profile_pic');
public function post_profile_pic(Request $request) {
$response = $this->app->json(array(
'file' => $request
));
return $response;
}
I'm using Postman to upload the file (see screenshot), but the request is empty:
{
"file": {
"attributes": { },
"request": { },
"query": { },
"server": { },
"files": { },
"cookies": { },
"headers": { }
}
}
Yet it obviously knows that there should be a file there. So how do I access the file?
Uploaded files in PHP are not sent with $_POST but in a separate variable called $_FILES ( http://php.net/manual/en/features.file-upload.post-method.php ) - so in your case you should take a look at
$this->app->files
If you use the default form components from symfony the documentation is at http://symfony.com/doc/current/reference/forms/types/file.html#basic-usage
In the simpleforms module files are retrieved by the lines following
$files = $this->app['request']->files->get($form->getName());
Which is using the basic structure that the Bolt, Silex and Symfony components provide.
(See also https://github.com/jadwigo/SimpleForms/blob/master/Extension.php#L505)

Resources