Why using additive paramerer in Resource collection raised error? - laravel

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.

Related

Unable to retrieve the file_size for file when using Storage::fake for testing

I just upgraded my app from Laravel v8 to v9. Now I'm having an issue with the following code throwing this error:
League\Flysystem\UnableToRetrieveMetadata : Unable to retrieve the file_size for file at location: test_file.xlsx.
My controller:
<?php
namespace App\Http\Controllers\Reports;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Storage;
class ReportDownloadController extends Controller
{
/**
* Handle the incoming request.
*
* #param string $path
* #return \Symfony\Component\HttpFoundation\StreamedResponse
*/
public function __invoke(string $path): \Symfony\Component\HttpFoundation\StreamedResponse
{
return Storage::disk('temp')->download($path, request()->query('filename'));
}
}
The relevant test:
/** #test */
public function it_returns_a_file_download_with_the_provided_filename_presented_to_the_user()
{
$this->withoutExceptionHandling();
Storage::fake('temp');
$fakeFile = UploadedFile::fake()->create('test_file.xlsx');
Storage::disk('temp')->put('test_file.xlsx', $fakeFile);
$response = $this->actingAs($this->user)
->getJson(route('reports.download', ['path' => 'test_file.xlsx', 'filename' => 'Appropriate.xlsx']));
$response->assertDownload('Appropriate.xlsx');
}
I know Flysystem was updated to v3, but I can't seem to figure out how to resolve this issue.
I even created an empty Laravel 9 app to test with the following code and still get the same error, so I don't think it is my app.
public function test_that_file_size_can_be_retrieved()
{
Storage::fake('local');
$fakeFile = UploadedFile::fake()->create('test_file.xlsx', 10);
Storage::disk('local')->put('test_file.xlsx', $fakeFile);
$this->assertEquals(10, (Storage::disk('local')->size('test_file.xlsx') / 1024));
}
What am is missing?
I've had the same problem when switching from Laravel 8 to Laravel 9.
The error Unable to retrieve the file_size was actually thrown because it couldn't find the file at all at the path provided.
In my case, it turned out that the path that was provided to the download function started with a /, which wasn't cause for problem with Flysystem 2 but apparently is with Flysystem 3.
Given the tests you're showing it doesn't look like it's the issue here but maybe this will help anyway.
So it turns out my test was broken all along. I should have been using $fakeFile->hashName() and I had the filename where I should have had '/' for the path in my Storage::put().
Here are the corrected tests that pass as expected:
/** #test */
public function it_returns_a_file_download_with_the_provided_filename_presented_to_the_user()
{
$this->withoutExceptionHandling();
Storage::fake('temp');
$fakeFile = UploadedFile::fake()->create('test_file.xlsx');
Storage::disk('temp')->put('/', $fakeFile);
$response = $this->actingAs($this->user)
->getJson(route('reports.download', ['path' => $fakeFile->hashName(), 'filename' => 'Appropriate.xlsx']));
$response->assertDownload('Appropriate.xlsx');
}
and
public function test_that_file_size_can_be_retrieved()
{
Storage::fake('local');
$fakeFile = UploadedFile::fake()->create('test_file.xlsx');
Storage::disk('local')->put('/', $fakeFile);
$this->assertEquals(0, Storage::disk('local')->size($fakeFile->hashName()));
}
Thanks, #yellaw. Your comment about it not being able to find the file at the path provided helped me realize I had done something stupid.

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.

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();

Laravel create GUID for specific column

I would like to create a UUID/GUID for a specific column in Laravel 5. So i am using the Library from Webpatser to get an UUID in Laravel5.
I would like to set this as default for a specific column, so i guess i should use attributes, or?
But when i use
protected $attributes = array('guid' => Uuid::generate(4)->string);
ill always get an error with:
syntax error, unexpected '(', expecting ')'
I am not sure, because the syntax looks fine and when ill try
protected $attributes = array('guid' => 'dsadasfasfsaf');
Everything works fine (guid is a varchar(36) field) - and
Uuid::generate(4)->string
returns a string.
Do i need anything else to create a default value for my model? Thanks in advance.
PHP can't parse non-trivial expressions in initializers.
You can do this:
class YourClass
{
protected $attributes;
function __construct()
{
$this->attributes = array('guid' => Uuid::generate(4)->string);
}
}
Or this by using the setter method:
class YourClass
{
protected $attributes;
public function setAttributes($attributes)
{
$this->attributes = $attributes;
}
}
$classInstance = new YourClass;
$classInstance->setAttributes(array('guid' => Uuid::generate(4)->string));

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