I am trying to implement a custom transformer using dingo api (https://github.com/dingo/api/wiki/Transformers#custom-transformation-layer) for my Post model and I am getting this exception:
Missing argument 2 for PostTransformer::transform(), called in /home/.../vendor/league/fractal/src/Scope.php on line 298 and defined
My controller:
$post = Post::findOrFail(2);
return $this->item($post, new PostTransformer);
My PostTransformer class:
<?php
use Illuminate\Http\Request;
use Dingo\Api\Transformer\Binding;
use Dingo\Api\Transformer\TransformerInterface;
class PostTransformer implements TransformerInterface
{
public function transform($response, $transformer, Binding $binding, Request $request)
{
// Make a call to your transformation layer to transformer the given response.
return [
'kkk' => 'val'
];
}
}
What is wrong?
Your PostTransformer isn't a Transformer. What you specified there is an TransformerLayer (https://github.com/dingo/api/wiki/Transformers#custom-transformation-layer).
However a Transformer in Dingo looks like this:
<?php
use League\Fractal\TransformerAbstract;
class PostTransformer extends TransformerAbstract
{
public function transform(Post $post) {
return [
'id' => $post->id
// ...
];
}
}
Related
I have created an API Resource:
class OrderResource extends JsonResource
{
public function toArray($request)
{
return [
"id" => $this->Id,
"photo" => ''
];
}
}
In controller I get data from model OrderModel the put data into resource OrderResource:
public function show($id)
{
$order = OrderModel::with('OrderPhoto')->findOrFail(1);
return new OrderResource($order);
}
So, I tried to use relation OrderPhoto in OrderResource like this:
public function toArray($request)
{
return [
"id" => $this->Id,
"photo" => OrderPhotoResource::collection($this->whenLoaded('OrderPhoto')),
];
}
But it does not work and gives this error:
Undefined property: Illuminate\Database\Query\Builder::$map
I did dd($this) in resource and what I got:
Class OrderPhoto:
class OrderPhoto extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return parent::toArray($request);
}
}
TL;DR
Try this in your OrderResource:
use OrderPhoto as OrderPhotoResource;
//
public function toArray($request)
{
return [
"id" => $this->Id,
"photo" => new OrderPhotoResource($this->whenLoaded('OrderPhoto')),
];
}
Explanation
As you can see, you are already defining the OrderPhoto as a Resource Collection:
class OrderPhoto extends ResourceCollection // <-- note the extended class
So in this case, you'll need to use this class instanciating it and pass in it the collection, instead of using the static method collection.
When you define a API Resource for a single object, like this:
php artisan make:resource PostResource
you use it like below:
$post = Post::find(1);
return new PostResource($post);
And if you want to use an API Resource to format a collection of resources instead of a single one, you need to do this:
$posts = Post::all();
return PostResource::collection($posts); // <-- note the ::collection part
Controlling the metadata
If you want to have a total control of the returned metadata in the response, define a custom API Resource Collection class instead.
Generate the class as a collection (adding the 'Collection' at the end or using the flag --collection):
php artisan make:resource PostResourceCollection
then, after customize it:
$posts = Post::all();
return new PostResourceCollection($posts); // <-- instantiating the class
I am having some trouble with route model binding my Eloquent subclass. The following code works fine:
$repo = new \App\Repositories\Eloquent\PluginRepository();
$plugin = $repo->findOrFail(1);
var_dump($plugin->type);
Output
object(App\PluginsTypes)#360 (26) {...}
But when I make a model bind, like this:
routes/web.php
Route::resource('plugins', 'PluginsController');
app/Http/Controllers/Admin/PluginsController.php
public function edit(PluginRepositoryInterface $plugin){
var_dump($plugin); // object(App\Repositories\Eloquent\PluginRepository)#345 (26) {...}
var_dump($plugin->id); // NULL
}
So the problem is, that it does not find the id passed in the route.
Addition code in Laravel project:
app/Plugins.php
<?php
namespace App;
class Plugins extends Model{
// My Eloquent Model
/**
* The foreignKey and ownerKey needs to be set, for the relation to work in subclass.
*/
public function type(){
return $this->belongsTo(PluginsTypes::class, 'plugin_type_id', 'id');
}
}
app/Repositories/SomeRepository.php
<?php
namespace App\Repositories;
use App\Abilities\HasParentModel;
class PluginsRepository extends Plugins{
protected $table = 'some_table';
use HasParentModel;
}
config/app.php
'providers' => [
...
App\Repositories\Providers\PluginRepositoryServiceProvider::class,
...
]
app/Repositories/Providers/PluginRepositoryServiceProvider.php
<?php
namespace App\Repositories\Providers;
use Illuminate\Support\ServiceProvider;
class PluginRepositoryServiceProvider extends ServiceProvider{
/**
* This registers the plugin repository - added in app/config/app.php
*/
public function register(){
// To change the data source, replace the concrete class name with another implementation
$this->app->bind(
'App\Repositories\Contracts\PluginRepositoryInterface',
'App\Repositories\Eloquent\PluginRepository'
);
}
}
Been using these resources:
HasParentModel Trait on GitHub
Extending Models in Eloquent
I found the answer in the docs (of course):
https://laravel.com/docs/5.6/routing#route-model-binding in the section Customizing The Resolution Logic
In my app/Repositories/Providers/PluginRepositoryServiceProvider.php i have added the following under my interface binding and it now works.
$this->app->router->bind('plugin', function ($value) {
return \App\Repositories\Eloquent\PluginRepository::where('id', $value)->first() ?? abort(404);
});
I will probably rename it, but it work like a charm :) Good day...
I am following a tutorial to write 2 classes for filtering threads in a forum application.I got this error in line
$threads = Thread::latest()->filter($filters); // in threadscontroller
Error:
Method Illuminate\Database\Query\Builder::filter does not exist.
ThreadsController with index method:
<?php
namespace App\Http\Controllers;
use App\Thread;
use App\Channel;
use App\Filters\ThreadFilters;
use Illuminate\Http\Request;
class ThreadsController extends Controller
{
public function __construct(){
$this->middleware('auth')->only('store','create');
}
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index(Channel $channel,ThreadFilters $filters)
{
$threads = Thread::latest()->filter($filters);
if($channel->exist){
$threads->where('channel_id',$channel->id);
}
$threads = $threads->get();
return view('threads.index',compact('threads'));
}
This is the abstract class Filters:
<?php
namespace App\Filters;
use Illuminate\Http\Request;
abstract class Filters{
protected $request;
protected $builder;
protected $filters = [];
public function __construct(Request $request){
$this->request = $request;
}
public function apply($builder){
$this->builder = $builder;
foreach($this->getFilters() as $filter=>$value){ //filter by,value yunus mesela.
if(method_exist($this,$filter)){
$this->$filter($value);
}
}
return $this->builder;
}
public function getFilters(){
return $this->request->intersect($this->filters);
}
}
Here ThreadFilters.php which extends filters class:
<?php
namespace App\Filters;
use App\User;
use Illuminate\Http\Request;
class ThreadFilters extends Filters
{
protected $filters =['by'];
protected function by($username){
$user = User::where('name',$username)->firstorFail();
return $this->builder->where('user_id',$user->id);
}
}
If I change latest to all, I get this error:
Type error: Argument 1 passed to
Illuminate\Support\Collection::filter() must be callable or null,
object given, called in
Also can anyone explain me what is $builder doing in those classes?
latest() is a modifier shortcut, equivalent to orderBy('created_at', 'desc'). All it does is add the ORDER BY constraint to the query.
filter() is a method on the Collection class. That method does not exist in the query builder, hence the "method not found" error you're receiving.
It does not appear that your filter class should be used with the resulting Collection. Rather, it adds conditionals to your original query. Try implementing it like this:
// Remove the filters() method here.
$threads = Thread::latest();
if ($channel->exist) {
$threads->where('channel_id', $channel->id);
}
// Pass your query builder instance to the Filters' apply() method.
$filters->apply($threads);
// Perform the query and fetch results.
$threads = $threads->get();
Also, for future questions, including the tutorial you're attempting/following can provide beneficial context to those helping you. :)
If you change latest to all, you're getting a Laravel Collection. So you are calling filter() on a Collection ($threads = Thread::all()->filter($filters);).
If you take a look into the code, you'll see, that the where() method of the array class gets called, which calls PHP's array_filter method. As you can see, a callable must be given.
But you are passing an Object to the filter method, $filters, which is an ThreadFilters-Object -> method injection here:
public function index(Channel $channel,ThreadFilters $filters) ...
Your error message answers your question in a great way:
Type error: Argument 1 passed to Illuminate\Support\Collection::filter() must be callable or null, object given, called in
Using a REST approach I want to be able to save more than one model in a single action.
class MyController extends ActiveController {
public $modelClass = 'models\MyModel';
}
class MyModel extends ActiveRecord {
...
}
That automagically creates actions for a REST api. The problem is that I want to save more than one model, using only that code in a POST will result in a new record just for MyModel. What if I need to save AnotherModel?
Thanks for any suggestion.
ActiveController implements a common set of basic actions for supporting RESTful access to ActiveRecord. For more advanced use you will need to override them or just merge to them your own custom actions where you will be implementing your own code & logic.
Check in your app the /vendor/yiisoft/yii2/rest/ folder to see how ActiveController is structured and what is doing each of its actions.
Now to start by overriding an ActiveController's action by a custom one, you can do it within your controller. Here is a first example where i'm overriding the createAction:
1-
class MyController extends ActiveController
{
public $modelClass = 'models\MyModel';
public function actions()
{
$actions = parent::actions();
unset($actions['create']);
return $actions;
}
public function actionCreate(){
// your code
}
}
2-
Or you can follow the ActiveController's structure which you can see in /vendor/yiisoft/yii2/rest/ActiveController.php by placing your custom actions in separate files. Here is an example where I'm overriding the updateAction by a custom one where i'm initializing its parameters from myController class :
class MyController extends ActiveController
{
public $modelClass = 'models\MyModel';
public function actions() {
$actions = parent::actions();
$custom_actions = [
'update' => [
'class' => 'app\controllers\actions\WhateverAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'scenario' => $this->updateScenario,
'params' => \Yii::$app->request->bodyParams,
],
];
return array_merge($actions, $custom_actions);
}
}
Now let's say as example that in my new action file app\controllers\actions\WhateverAction.php I'm expecting the Post Request (which i'm storing in $params) to have a subModels attribute storing a list of child models to which I'm going to apply some extra code like relating them with their parent model if they already exists in first place :
namespace app\controllers\actions;
use Yii;
use yii\base\Model;
use yii\db\ActiveRecord;
use yii\web\ServerErrorHttpException;
use yii\rest\Action;
use app\models\YourSubModel;
class WhateverAction extends Action
{
public $scenario = Model::SCENARIO_DEFAULT;
public $params;
public function run($id)
{
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
$model->scenario = $this->scenario;
$model->load($this->params, '');
foreach ($this->params["subModels"] as $subModel) {
/**
* your code related to each of your model's posted child
* for example those lines will relate each child model
* to the parent model by saving that to database as their
* relationship has been defined in their respective models (many_to_many or one_to_many)
*
**/
$subModel = YourSubModel::findOne($subModel['id']);
if (!$subModel) throw new ServerErrorHttpException('Failed to update due to unknown related objects.');
$subModel->link('myParentModelName', $model);
//...
}
// ...
return $model;
}
}
So if I understand you wish to add a new database entry not only for the model you are querying, but for another model.
The best place to do this would be in the AfterSave() or BeforeSave() functions of the first model class. Which one would depend on the data you are saving.
Struggling using parseIncludes in https://github.com/thephpleague/fractal.
I have two tables, Property and Weeks. Each property has many weeks. Using Fractal I can return my property item with a collection of weeks. What I want to do is use parseIncludes, so that the return of weeks is optional.
PropertyTransformer.php
<?php
namespace App\Transformer;
use App\Models\Property;
use League\Fractal\TransformerAbstract;
class PropertyTransformer extends TransformerAbstract
{
protected $availableIncludes = [
'week'
];
public function transform(Property $property)
{
return [
'id' => (int) $property['PropertyID'],
'PropertyName' => $property['PropertyName'],
'ExactBeds' => (int) $property['ExactBeds'],
'weeks' => $property->week
];
}
/**
* Include Week
*
* #return League\Fractal\ItemResource
*/
public function includeWeek( Property $property )
{
$week = $property->week;
return $this->item($week, new WeekTransformer);
}
}
WeekTransformer.php
<?php
namespace App\Transformer;
use App\Models\Week;
use League\Fractal;
class WeekTransformer extends Fractal\TransformerAbstract
{
public function transform(Week $week)
{
return [
'Week' => $week['week'],
'Available' => $week['available'],
'Price' => (int) $week['price'],
];
}
}
My PropertyController.php
<?php namespace App\Http\Controllers\Api\v1;
use App\Http\Requests;
use App\Models\Week;
use Illuminate\Support\Facades\Response;
use App\Models\Property;
use League\Fractal;
use League\Fractal\Manager;
use League\Fractal\Resource\Collection as Collection;
use League\Fractal\Resource\Item as Item;
use App\Transformer\PropertyTransformer;
class PropertyController extends \App\Http\Controllers\Controller {
public function show($id)
{
$property = Property::with('bedroom')->with('week')->find($id);
$fractal = new Fractal\Manager();
if (isset($_GET['include'])) {
$fractal->parseIncludes($_GET['include']);
}
$resource = new Fractal\Resource\Item($property, new PropertyTransformer);
//$resource = new Fractal\Resource\Collection($properies, new PropertyTransformer);
return $fractal->createData( $resource )->parseIncludes('weeks')->toJson();
}
I get the following error on the parseIncludes:-
Method 'parseIncludes' not found in class \League\Fractal\Scope
I'm following the guide here on transformers - http://fractal.thephpleague.com/transformers/
I think I am going wrong somewhere here where it says:-
These includes will be available but can never be requested unless the Manager::parseIncludes() method is called:
<?php
use League\Fractal;
$fractal = new Fractal\Manager();
if (isset($_GET['include'])) {
$fractal->parseIncludes($_GET['include']);
}
If I remove the parseIncludes, I don't get an error, I also get my property data with my collection of weeks, but ?include=week doesn't work to optionally get it.
Your problem is in this line:
return $fractal->createData( $resource )->parseIncludes('weeks')->toJson();
createData() returns \League\Fractal\Scope and it has no parseInlcudes method.
You've already called parseIncludes here:
if (isset($_GET['include'])) {
$fractal->parseIncludes($_GET['include']);
}
So just remove the second call to it in the return statement:
return $fractal->createData($resource)->toJson();