Operation without entity - api-platform.com

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.

Related

how can I pass parameter to laravel getAttribute when I am appending it

this is my laravel custom accessor which I am appending using
protected $appends = [leave_balances];
public function getLeaveBalancesAttribute() {
// some code
}
I want to pass a parameter when I am calling this accessor like this
public function getLeaveBalancesAttribute($parameter) {
// use $parameter here
}
$payslip = Payslip::find(1);
\Log::debug($payslip->leave_balances("PARAMETER"));
I have searched and found that it is not possible. please can some one provide any solution to this I need to pass this parameter.
you dont append attribute unless you want it to act as an attribute,
you can just create a method since you are calling it like a method
in you Payslip model
public function leaveBalances( $params ) {
return $params
}
then you can use it like
$payslip = Payslip::find(1);
$payslip->leaveBalances("PARAMETER") // which output PARAMETER
If you declare an Attribute, you can only use it like this (following your example:
protected $appends = ['leave_balances'];
public function getLeaveBalancesAttribute()
{
return 'Hi!';
}
$payslip = Payslip::find(1);
$value = $payslip->leave_balances;
dd($value); // This will output string(Hi!)
What you (I think) want is setLeaveBalancesAttribute, so you can pass a value and do whatever you want with it:
public function setLeaveBalancesAttribute($parameter)
{
return $parameter.' Yes!';
}
$payslip = Payslip::find(1);
$payslip->leave_balances = 'It works!';
dd($payslip->leave_balances); // This will output string(It works! Yes!)
But, if you are using Laravel 9+, please do use the new way of defining attributes, it is better.
You can set the attribute $appends in the model where you have the accessor. Something like this:
protected $appends = ['the name of accessor'];
However, it will be in the most, I think in all, the responses or query you do with the model you declare it.
Another options is creating a single instance of the model using the ::find method. For example:
$model_instance = Model::find($id);
$attribute = $model_instance->attribute;
Here is the documentation reference: https://laravel.com/docs/9.x/eloquent-mutators#defining-an-accessor

Why using additive paramerer in Resource collection raised error?

In laravel 9 app I want to add additive paramerer into Resource and looking at this
Laravel 5.6 - Pass additional parameters to API Resource?
branch I remade in app/Http/Resources/CurrencyResource.php :
<?php
namespace App\Http\Resources;
use App\Library\Services\DateFunctionalityServiceInterface;
use Illuminate\Http\Resources\Json\JsonResource;
use App;
use Illuminate\Support\Facades\File;
use Spatie\Image\Image;
use App\Http\Resources\MediaImageResource;
class CurrencyResource extends JsonResource
{
protected $show_default_image = false;
public function showDefaultImage($value){
$this->show_default_image = $value;
return $this;
}
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
*
* #return array
*/
public function toArray($request)
{
$dateFunctionality = App::make(DateFunctionalityServiceInterface::class);
$currencyImage = [];
$currencyMedia = $this->getFirstMedia(config('app.media_app_name'));
if ( ! empty($currencyMedia) and File::exists($currencyMedia->getPath())) {
$currencyImage['url'] = $currencyMedia->getUrl();
$imageInstance = Image::load($currencyMedia->getUrl());
$currencyImage['width'] = $imageInstance->getWidth();
$currencyImage['height'] = $imageInstance->getHeight();
$currencyImage['size'] = $currencyMedia->size;
$currencyImage['file_title'] = $currencyMedia->file_name;
}
else {
\Log::info( varDump($this->show_default_image, ' -1 $this->show_default_image::') );
$currencyImage['url'] = $this->show_default_image ? '/images/default-currency.jpg' : '';
}
// $currencyMedia = $currency->getFirstMedia(config('app.media_app_name'));
return [
'id' => $this->id,
'name' => $this->name,
...
and with code in control :
'currency' => (new CurrencyResource($currency))->showDefaultImage(false),
its work ok, but I got an error :
Method Illuminate\Support\Collection::showDefaultImage does not exist.
when I applyed this method for collection:
return (CurrencyResource::collection($currencies))->showDefaultImage(true);
But in link above there is a similar way :
UserResource::collection($users)->foo('bar');
What is wrong in my code and how that can be fixed ?
Thanks!
I wonder if there is a reason you can't use this approach: https://stackoverflow.com/a/51689732/8485567
Then you can simply use the request parameters to modify your response.
If you really want to get that example working in that way, it seems you are not following the example correctly.
You need to override the static collection method inside your CurrencyResource class:
public static function collection($resource){
return new CurrencyResourceCollection($resource);
}
You also need to create the CurrencyResourceCollection class and define the showDefaultImage method and $show_default_image property on that class as in the example you referred to.
Then you should be able to do:
CurrencyResource::collection($currencies)->showDefaultImage(true);
The reason the way you are doing it doesn't work is because you haven't defined the static collection method on your resource hence it's defaulting to the normal behavior of returning a default collection object as you can see in your error message.

How to define custom message on testing JSON in Laravel if assertation fails?

I have this test in my Laravel project:
// here are some test function configurations
// found nothing if not match
$this->post(action([TagController::class, 'search'], '!! Not foundable name !!'))
->assertOk()
->assertJsonCount(0, 'data');
// found it if match
$this->post(action([TagController::class, 'search'], $matchName))
->assertOk()
->assertJsonCount(1, 'data');
// found it if partly match
$this->post(action([TagController::class, 'search'], $partlyMatchName))
->assertOk()
->assertJsonCount(1, 'data');
Now I see this result if test failed:
Failed to assert that the response count matched the expected 0
Failed asserting that actual size 4 matches expected size 0.
This isn't say too mutch for me, I don't see which assertation failed and exactly why. I want to define custom message for this case.
I want to do someting like this:
->assertJsonCount(
0,
'data',
'It should found noting, because of conditions are not match'
);
Is there any way to send custom message to the tester user in this case?
You should override decodeResponseJson() method of Illuminate\Testing\TestReponse class.
Create two classes of TestResponse and AssertableJsonString in Tests namespace as follows:
TestResponse
namespace Tests;
use Illuminate\Testing\Assert as PHPUnit;
class TestResponse extends \Illuminate\Testing\TestResponse
{
/**
* #inheritDoc
*/
public function decodeResponseJson()
{
$testJson = new AssertableJsonString($this->getContent());
$decodedResponse = $testJson->json();
if (is_null($decodedResponse) || $decodedResponse === false) {
if ($this->exception) {
throw $this->exception;
} else {
PHPUnit::fail('Invalid JSON was returned from the route.');
}
}
return $testJson;
}
}
AssertableJsonString
namespace Tests;
use Illuminate\Testing\Assert as PHPUnit;
class AssertableJsonString extends \Illuminate\Testing\AssertableJsonString implements \ArrayAccess, \Countable
{
/**
* #inheritDoc
*/
public function assertCount(int $count, $key = null, $message="Failed to assert that the response count matched the expected %d")
{
if (! is_null($key)) {
PHPUnit::assertCount(
$count, data_get($this->decoded, $key),
sprintf($message, $count)
);
return $this;
}
PHPUnit::assertCount($count,
$this->decoded,
sprintf($message, $count)
);
return $this;
}
}
Now, you need to bind Laravel TestResponse class to your custom TestResponse in boot method of AppServiceProvider as follows:
public function boot()
{
$this->app->bind(\Illuminate\Testing\TestResponse::class,\Tests\TestResponse::class);
}
Notice: You need to place %d in your message format to be replaced by sprintf() function.

Impossible to get my $params in Laravel scope

I'm new in Laravel and I try to do a list a table. On this list I have some filters.
I try to use scope but It doesn't work.
In my controller I have :
class GrillesController extends Controller
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index(request $request)
{
$query = DB::table('grilles')
->join('compets as CO','CO.id','=','gri_compet_id')
->join ('journees as JO', 'JO.id','=','gri_journee_id')
->select('com_nm_logo','jou_l_journee_c','grilles.*')
->where('com_c_st','A')
->orderbyRaw('gri_d_deb,gri_h_deb,com_n_ord_aff');
$params = $request->except('_token');
$grilles = Grilles::FilterListe($query,$params)->get();
In my model :
public function scopeFilterListe($query,$params)
{
$value = $params['filter_compets'];
$query->where('gri_compet_id','=',$value);
return $query;
}
In scopeFilterListe in the model, I'd like to get all my variable of my URL to build my where clause.
http://xxx.fr/grilles?_token=DMcay7SkDc1QeFlxRPMqT4DTGJXdgcXRbVbCNRvo&filter_compets=1&filter_etatparis=3&filter_periode=
I tried a lot of things, but I always have this error:
"Cannot use object of type Illuminate\Database\Query\Builder as
array"
It comes from : $value = $params['filter_compets'];
If I write $value=1, it works...(even if I have another problem as the data supposed to come from
->join('compets as CO','CO.id','=','gri_compet_id')
->join ('journees as JO', 'JO.id','=','gri_journee_id')
are not displayed..
What is the problem for my scope ?
Thanks a lot for your help.
I think that you need dynamic scope. Please find documentation here.
Herewith your modified script.
public function scopeOfFilterListe($query,$value)
{
$query->where('gri_compet_id','=',$value);
return $query;
}
$query = DB::table('grilles')
->join('compets as CO','CO.id','=','gri_compet_id')
->join ('journees as JO', 'JO.id','=','gri_journee_id')
->select('com_nm_logo','jou_l_journee_c','grilles.*')
->where('com_c_st','A')
->orderbyRaw('gri_d_deb,gri_h_deb,com_n_ord_aff')
;
$params = $request->except('_token');
$grilles = Grilles::ofFilterListe($params['filter_compets'])->get();

What is the difference between trait and behavior in cakephp 3?

I find soft delete in cakephp 3 that implemented via traits. And I try to implement it via behaviors. But unlike the trait version, SoftDeleteBehavior do not work.
I have this line in my model initialize method:
$this->addBehavior('SoftDelete');
And this is my SoftDeleteBehavior
namespace App\Model\Behavior;
use Cake\ORM\Behavior;
use Cake\ORM\RulesChecker;
use Cake\Datasource\EntityInterface;
use App\Model\Behavior\MyQuery;
class SoftDeleteBehavior extends Behavior {
public $user_id = 1;
public function getDeleteDate() {
return isset($this->deleteDate) ? $this->deleteDate : 'deleted';
}
public function getDeleter() {
return isset($this->deleter) ? $this->deleter : 'deleter_id';
}
public function query() {
return new MyQuery($this->connection(), $this);
}
/**
* Perform the delete operation.
*
* Will soft delete the entity provided. Will remove rows from any
* dependent associations, and clear out join tables for BelongsToMany associations.
*
* #param \Cake\DataSource\EntityInterface $entity The entity to soft delete.
* #param \ArrayObject $options The options for the delete.
* #throws \InvalidArgumentException if there are no primary key values of the
* passed entity
* #return bool success
*/
protected function _processDelete($entity, $options) {
if ($entity->isNew()) {
return false;
}
$primaryKey = (array)$this->primaryKey();
if (!$entity->has($primaryKey)) {
$msg = 'Deleting requires all primary key values.';
throw new \InvalidArgumentException($msg);
}
if (isset($options['checkRules']) && !$this->checkRules($entity, RulesChecker::DELETE, $options)) {
return false;
}
$event = $this->dispatchEvent('Model.beforeDelete', [
'entity' => $entity,
'options' => $options
]);
if ($event->isStopped()) {
return $event->result;
}
$this->_associations->cascadeDelete(
$entity,
['_primary' => false] + $options->getArrayCopy()
);
$query = $this->query();
$conditions = (array)$entity->extract($primaryKey);
$statement = $query->update()
->set([$this->getDeleteDate() => date('Y-m-d H:i:s') , $this->getDeleter() => $this->user_id])
->where($conditions)
->execute();
$success = $statement->rowCount() > 0;
if (!$success) {
return $success;
}
$this->dispatchEvent('Model.afterDelete', [
'entity' => $entity,
'options' => $options
]);
return $success;
}
If I use trait, SoftDeleteTrait works in true manner. But SoftDeleteBehavior do not work properly!
One is a PHP language construct, the other is a programmatic concept. You may want to read upon what traits are, so that you understand that this question, as it stands, doesn't make too much sense. Also stuff like "doesn't work" doesn't serve as a proper problem description, please be more specific in the future.
That being said, CakePHP behaviors do serve the purpose of horizontal code reuse, similar to traits, as opposed to vertical reuse by inheritance.
However, even if they have conceptual similarities, you cannot simply exchange them as you seem to do in your code, a trait will be composited into the class on which it is used, so that it becomes part of it as if it were written directly in the class definition, and therefore has the ability to overwrite inherited code like the Table::_processDelete() method, a behavior on the other hand is a totally independent class, which is being instantiated and injected as a dependency into a table class at runtime, and calls to its methods are being delegated via the table class (see Table::__call()), unless a method with the same name already exists on the table class, which in your case means that _processDelete() will never be invoked.
I'd suggest that you study a little more on PHP/OOP basics, as this is rather basic stuff that can be untangled easily by just having a look at the source. Being able to understand how the CakePHP code base and the used concepts do work will make your life much easier.

Resources