Override doctrine pagination to perform slice in API Platform PaginationExtension - doctrine

API Platform has a built-in Doctrine pagination extension that returns a slice of the results given page_number and records_per_page, e.g. page 3, 50 records.
The getPagination() method of PaginationExtension.php convert the input to offset/length, e.g. offset=100, maxResults=50
// vendor/api-platform/core/src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php:121
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
{
if (null === $pagination = $this->getPagination($queryBuilder, $resourceClass, $operationName, $context)) {
return;
}
[$offset, $limit] = $pagination;
$queryBuilder
->setFirstResult($offset)
->setMaxResults($limit);
}
How to I create a class that overrides applyToCollection so that instead of returning a paginated result, it returns a slice, e.g. 220 records starting at 41.
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
{
[$offset, $limit] = [41, 220];
$queryBuilder
->setFirstResult($offset)
->setMaxResults($limit);
}
The records_per_page already exists, I can reuse that, I need to add an offset (to the context?), and override this method. I'm not sure if this is a custom pagination extension, or a custom data collector.

Related

Operation without entity

I've been looking for a solution for a while but none of the one I find really allows me to do what I want. I would just like to create routes that don't necessarily require an entity or id to be used. Can you help me the documentation is not clear to do this.
Thank you beforehand.
As you can read in the General Design Considerations, just make an ordinary PHP class (POPO). Give it an ApiResource annontation like this:
* #ApiResource(
* collectionOperations={
* "post"
* },
* itemOperations={}
* )
Make sure the folder your class is in is in the paths list in api/config/packages/api_platform.yaml. There usually is the following configuration:
api_platform:
mapping:
paths: ['%kernel.project_dir%/src/Entity']
You should add your path if your class is not in the Entity folder.
Api Platform will expect json to be posted and try to unserialize it into an instance of your class. Make a custom DataPersister to process the instance, for example if your class is App\ApiCommand\Doit:
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use App\ApiCommand\Doit;
use App\ApiResult\DoitResult;
final class DoitDataPersister implements ContextAwareDataPersisterInterface
{
public function supports($data, array $context = []): bool
{
return $data instanceof Doit;
}
public function persist($data, array $context = [])
{
// code to process $data
$result = new DoitResult();
$result->description = 'Hello world';
return $result;
}
public function remove($data, array $context = [])
{
// will not be called if you have no delete operation
}
}
If you need Doctrine, add:
public function __construct(ManagerRegistry $managerRegistry)
{
$this->managerRegistry = $managerRegistry;
}
See Injecting Extensions for how to use it.
Notice that the result returned by ::persist is not an instance of Doit. If you return a Doit api platform will try to serialize that as the result of your operation. But we have marked Doit as an ApiResource so (?) api platform looks for an item operation that can retrieve it, resulting in an error "No item route associated with the type App\ApiCommand\Doit". To avoid this you can return any object that Symfonies serializer can serialize that is not an ApiResource. In the example an instance of DoitResult. Alternatively you can return an instance of Symfony\Component\HttpFoundation\Response but then you have to take care of the serialization yourself.
The post operation should already work, but the swagger docs are made from metadata. To tell api platform that it should expect a DoitResult to be returned, change the #ApiResource annotation:
* collectionOperations={
* "post"={
* "output"=DoitResult::class
* }
* },
This will the add a new type for DoitResult to the swagger docs, but the descriptions are still wrong. You can correct them using a SwaggerDecorator. Here is one for a 201 post response:
namespace App\Swagger;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
final class SwaggerDecorator implements NormalizerInterface
{
private $decorated;
public function __construct(NormalizerInterface $decorated)
{
$this->decorated = $decorated;
}
public function normalize($object, string $format = null, array $context = [])
{
$summary = 'short explanation about DoitResult';
$docs = $this->decorated->normalize($object, $format, $context);
$docs['paths']['/doit']['post']['responses']['201']['description'] = 'Additional explanation about DoitResult';
$responseContent = $docs['paths']['/doit']['post']['responses']['201']['content'];
$this->setByRef($docs, $responseContent['application/ld+json']['schema']['properties']['hydra:member']['items']['$ref'],
'description', $summary);
$this->setByRef($docs, $responseContent['application/json']['schema']['items']['$ref'],
'description', $summary);
return $docs;
}
public function supportsNormalization($data, string $format = null)
{
return $this->decorated->supportsNormalization($data, $format);
}
private function setByRef(&$docs, $ref, $key, $value)
{
$pieces = explode('/', substr($ref, 2));
$sub =& $docs;
foreach ($pieces as $piece) {
$sub =& $sub[$piece];
}
$sub[$key] = $value;
}
}
To configure the service add the following to api/config/services.yaml:
'App\Swagger\SwaggerDecorator':
decorates: 'api_platform.swagger.normalizer.api_gateway'
arguments: [ '#App\Swagger\SwaggerDecorator.inner' ]
autoconfigure: false
If your post operation is not actually creating something you may not like the 201 response. You can change that by specifying the response code in the #ApiResource annotation, for example:
* collectionOperations={
* "post"={
* "output"=DoitResult::class,
* "status"=200
* }
* },
You may want to adapt the SwaggerDecorator accordingly.
Creating a "get" collection operation is similar, but you need to make a DataProvider instead of a DataPersister. The chapter9-api branch of my tutorial contains an example of a SwaggerDecorator for a collection response.
Thanks you for answer. I had some information but not everything. I will try the weekend.

Argument 2 passed to Spatie\Searchable\SearchResult::__construct() must be of the type string, null given

im using spatie/laravel-searchable for my website.
it works very well in this function:
public function index(Request $request)
{
$results = (new Search())
->registerModel(Product::class, 'name', 'price','barcode')
->registerModel(Category::class, 'name')
->registerModel(Catalog::class, 'name')
->registerModel(Color::class, 'fatitle','entitle')
->search($request->input('query'));
return response()->json($results);
}
but in some words(like:cu006), i have this error:
Argument 2 passed to Spatie\Searchable\SearchResult::__construct() must be of the type string, null given
vendor/spatie/laravel-searchable/src/SearchResult.php:19
public function __construct(Searchable $searchable, string $title, ?string $url = null)
In your model, when you create the getSearchResult function
public function getSearchResult(): SearchResult
{
return new \Spatie\Searchable\SearchResult(
$this,
$this->title
);
}
If you write $this->title you need to make sure that your model actually contains the title field, if it doesn't it'll give you that error.
public function getSearchResult(): SearchResult
{
$companySlug = currentCompanySlug();
$url = url('/'.$companySlug.'/'.config('global-search- url.'.class_basename($this)));
$null = null;
return new SearchResult($this, $this->field_name ?:$null, $url);
}
please add in your model.

Api Platform DTO object created by transformer is not getting persisted

Using Api Platform, I have a problem using an input class and its transformation.
The following documentation has been followed.
https://api-platform.com/docs/core/dto/#using-data-transfer-objects-dtos
After Data Transformer service executes transformation and returns an object of the correct class, the object that is picked up by api-platform appears to be empty, so it either fails validation, if validation is present, or persistence to the database fails - due its fields appear to be empty.
Here is a simplified code of DataTransformer service methods - it produces an object with hardcoded values:
public function transform($object, string $to, array $context = [])
{
$newCreativeElement = new CreativeElement();
$newCreativeElement->setKeyName("HARDCODED VALUE");
$newCreativeElement->setIntValue(42);
return $newCreativeElement;
}
public function supportsTransformation($object, string $to, array $context = []): bool
{
if ($object instanceof CreativeElement){
return false;
}
$result = CreativeElement::class === $to && null !== ($context['input']['class'] ?? null);
return $result;
}
Edit:
It's solved by 2.4 release.
Upgrade your composer.json and enjoy.
I have the same problem.
Something i tried is return for an array instead of an object transform method. It's working but not real solution.
it's appear that denormalizer is called 2 times : once for your transformer, and after to transform "CreativeElement" into "CreativeElement" by AbstractItemNormalizer
$context['api_denormalize'] = true;
$context['resource_class'] = $class;
$inputClass = $this->getInputClass($class, $context);
if (null !== $inputClass && null !== $dataTransformer = $this->getDataTransformer($data, $class, $context)) {
$data = $dataTransformer->transform(
parent::denormalize($data, $inputClass, $format, ['resource_class' => $inputClass] + $context),
$class,
$context
);
}
return parent::denormalize($data, $class, $format, $context);
Looking for solution too

Retrieve parent class within morph relationship

I have this code
//ImageableTrait
trait ImageableTrait
{
public function images()
{
return $this->morphMany(Image::class, 'imageable')
->orderBy('order', 'ASC');
}
}
//User
class User extend Model
{
use ImageableTrait;
}
//Post
class Post extend Model
{
use ImageableTrait;
}
class ImageCollection extends Collection
{
public function firstOrDefault()
{
if ($this->count() === 0) {
$image = new Image();
$image->id = 'default';
$image->imageable_type = '/* I need the parent className here */';
$image->imageable_id = '.';
}
return $this->first();
}
}
//Image
class Image extend Model
{
public function imageable
{
return $this->morphTo();
}
public function newCollection(array $models = [])
{
return new ImageCollection($models);
}
public function path($size)
{
//Here, there is some logic to build the image path and it needs
//the imageable_type attribute no matter if there is
//an image record in the database or not
return;
}
}
I want to be able to do so
$path = User::find($id)->images->firstOrDefault()->path('large');
But I can't figure out how to get the parent class name to build the path properly...
I tried with $morphClass or getMorphClass() but can't figure out how to use it properly or if it is even the right way to do it.
Any thoughts on that?
I think you can keep it simple and drop the ImageCollection class because there is already a firstOrNew method that seems to be what you're looking for.
The firstOrNew method accepts an array of attributes that you want to match. If you don't care about the attributes, you can pass an empty array. If there are no matches in the database, it'll make a new instance with the proper parent type.
$path = User::find($id)->images()->firstOrNew([])->path('large');
Note: I am calling the images() method to get the MorphMany object so that I can call the firstOrNew method. In other words, you need to add the parentheses. Otherwise, you get a Collection.
Edit: If you want to make things a bit simpler by automatically setting some default attributes, you can add this to your ImageableTrait:
public function imagesOrDefault()
{
$defaultAttributes = ['id' => 'default'];
return $this->images()->firstOrNew($defaultAttributes);
}
Then, you can do something like this: $path = User::find($id)->imagesOrDefault()->path('large');
Note that your default attributes must be fillable for this to work. Also, imageable_id and imageable_type will automatically be set to your parent's id and type.
If you want to set the default value for imageable_id to a period and not the parent's id, then you have to alter it a bit, and it will look a lot like your original code except this will go inside your ImageableTrait.
public function imagesOrDefault()
{
// First only gets one image.
// If you want to get all images, then change it to get.
// But if you do that, change the conditional check to a count.
$image = $this->images()->first();
if (is_null($image))
{
$image = new Image();
$image->id = 'default';
$image->imageable_type = $this->getMorphClass();
$image->imageable_id = '.';
}
return $image;
}
Ok guys I've found something that seems to work pretty good for now so I'll stick with that.
In the Image model, I've added some code when I make the new collection:
public function newCollection(array $models = [])
{
$morphClass = '';
$parent = debug_backtrace(false, 2)[1];
if (isset($parent['function']) AND $parent['function'] === 'initRelation') {
if (isset($parent['args'][0][0])) {
$morphClass = get_class($parent['args'][0][0]);
}
}
return new ImageCollection($models, $morphClass);
}
I then simply retrieve the morphClass through the constructor of the ImageCollection
private $morphClass;
public function __construct($items = [], $morphClass)
{
parent::__construct($items);
$this->morphClass = $morphClass;
}
public function firstOrDefault()
{
if ($this->count() === 0) {
$image = new Image();
$image->id = 'default';
$image->imageable_type = $this->morphClass;
$image->imageable_id = '.';
}
return $this->first();
}
This way, I can simply call the method like that
User::with('images')->get()->images->firstOrDefault()
This seems to work really great in many cases, if I have some issues at some times, I'll update this post.
i may be late for the party, but i kinda did a small trick for morph relationships where i had "media" as morph, i get the parent since "model_type" has the full string parent class string.
$model = new $media->model_type;
$media->model = $model->findOrFail($media->model_id);

Sort columns in joomla via populateState method

I'm sorting table columns in Joomla Backend. I adjust settings according to this tutorial.
As we can see it is suggested to override populateState method and manually obtain sorting options.
public function populateState() {
$filter_order = JRequest::getCmd('filter_order');
$filter_order_Dir = JRequest::getCmd('filter_order_Dir');
$this->setState('filter_order', $filter_order);
$this->setState('filter_order_Dir', $filter_order_Dir);
}
But I noticed that the native component com_content does not set these options explicitly in the model file administrator/components/com_content/models/articles.php.
protected function populateState($ordering = null, $direction = null)
{
// Initialise variables.
$app = JFactory::getApplication();
$session = JFactory::getSession();
............................................
............................................
............................................
// List state information.
parent::populateState('a.title', 'asc');
}
Instead it just invokes parent populateState. And in fact JModelList::populateState() includes this:
protected function populateState($ordering = null, $direction = null)
{
// If the context is set, assume that stateful lists are used.
if ($this->context) {
$app = JFactory::getApplication();
.....................................
.....................................
.....................................
$value = $app->getUserStateFromRequest($this->context.'.ordercol', 'filter_order', $ordering);
if (!in_array($value, $this->filter_fields)) {
$value = $ordering;
$app->setUserState($this->context.'.ordercol', $value);
}
$this->setState('list.ordering', $value);
// Check if the ordering direction is valid, otherwise use the incoming value.
$value = $app->getUserStateFromRequest($this->context.'.orderdirn', 'filter_order_Dir', $direction);
if (!in_array(strtoupper($value), array('ASC', 'DESC', ''))) {
$value = $direction;
$app->setUserState($this->context.'.orderdirn', $value);
}
$this->setState('list.direction', $value);
}
else {
$this->setState('list.start', 0);
$this->state->set('list.limit', 0);
}
}
So I'm trying to imitate the code of the native com_content. Thus I assume that
class CompViewData extends JView
{
function display($tpl = null)
{
$this->state = $this->get('State');
Will invoke parent JModelList::populateState() (so I'm not overriding it in the modal class) and set $this->setState('list.ordering', $value);. But for some reason when I invoke $this->state->get() in getListQuery() to build my SQL query with ordering
protected function getListQuery()
{
$orderCol = $this->state->get('list.ordering', 'id');
$orderDirn = $this->state->get('list.direction', 'asc');
This variables happen to be not defined.
What am I missing? I assume it is somehow connected with proper user session, but I don't have evidence whatsoever.
After just coming across the same issue I found that, as you said, the superclass populateState() does indeed have the behaviour defined. However, it also does a check to ensure your field is in the "whitelist".
if (!in_array($value, $this->filter_fields))
If you look at com_content you will see this section at the top of the model class (in your case models/articles.php):
public function __construct($config = array())
{
if (empty($config['filter_fields']))
{
$config['filter_fields'] = array(
'id', 'a.id',
'title', 'a.title',
//...(more fields here)
'publish_up', 'a.publish_up',
'publish_down', 'a.publish_down',
);
$app = JFactory::getApplication();
$assoc = isset($app->item_associations) ? $app->item_associations : 0;
if ($assoc)
{
$config['filter_fields'][] = 'association';
}
}
parent::__construct($config);
}
You will need to include this section so that the ModelList class knows that the 'ordering' field is in the whitelist. Obviously substitute the fields with those on which you wish to filter.
The Joomla JModelList defines populateState like this
protected function populateState($ordering = null, $direction = null)
It means that if you do not have populateState override in you class, this will be called but it gets no values. The minimum requirement is to set default values if you want to use ordering. You may completely delete this method from your class if you are not planning to use ordering at all.
So, minimum what you need is to interpolate in your class
protected function populateState($ordering = null, $direction = null) {
parent::populateState('id', 'ACS');
}
Otherwise you will not get anything in $state->get() or $this->state->get() unless you click on ordering column. Then parent's populateState will take variables from request.

Resources