I am using fractal (fractal.thephpleague.com) to develop an API with Laravel (laravel.com). It is an amazing library, by the way.
In certain web service, I need to return information of several nested models, which have 3 levels deep. That is, I have a Survey model which has many Survey Items, and each one of them has, in turn, many Survey Item Results (each one of a user). Well, I need the data from all of them, classified, that is:
"surveys": [
{
"id": 1,
...,
"items": [
{
"id": 14,
...,
"results": [
{
"id": 45,
...
},
{
...
}
]
},
{
...
}
]
},
{
...
}
}
With transformers and includes, I get the surveys and survey items info without problems, but I also need the survey item results...
That is, I need something like 2-level "nested" includes, to get the information of the third level.
My best approach, so far (only returning two levels: surveys and survey items). In my controller:
return fractal() -> transform(
Survey::where(...),
new SurveyTransformer()
) -> include(['SurveyItems']) -> respond();
Any help is much appreciated.
Thanks in advance.
Here's what I normally do
Survey Transformer
<?php
namespace App\Transformers;
use League\Fractal;
use App\Survey;
class SurveyTransformer extends Fractal\TransformerAbstract
{
/**
* List of resources possible to include
*
* #var array
*/
protected $availableIncludes = [
'items'
];
public function transform(Survey $survey)
{
return [
'id' => (int) $user->id,
];
}
/**
* Include Items
*
* #param App\Survey $survey
* #return League\Fractal\CollectionResource
*/
public function includeItems(Survey $survey)
{
$items = $survey->items;
if (!is_null($items)) {
return $this->collection($items, new ItemTransformer);
}
return;
}
}
Item Transformer
<?php
namespace App\Transformers;
use League\Fractal;
use App\Item;
class ItemTransformer extends Fractal\TransformerAbstract
{
/**
* List of resources possible to include
*
* #var array
*/
protected $availableIncludes = [
'results'
];
public function transform(Item $item)
{
return [
'id' => (int) $user->id,
];
}
/**
* Include results
*
* #param App\Item $item
* #return League\Fractal\CollectionResource
*/
public function includeResults(Item $item)
{
$results = $item->results;
if (!is_null($results)) {
return $this->collection($results, new ResultTransformer);
}
return;
}
}
On my base controller
/**
* get fractal tranformed data
* #param $resource
*/
protected function fractalResponse($resource, array $includes = [])
{
$manager = new Manager();
$manager->setSerializer(new DataArraySerializer()); //or what ever you like
if (sizeof($includes) == 0) {
$data = $manager->createData($resource)
->toArray();
} else {
$manager->parseIncludes($includes);
$data = $manager->createData($resource)
->toArray();
}
return $data;
}
then
$resource = new \League\Fractal\Resource\Collection($survies, new SurveyTransformer);
$response_data = $this->fractalResponse($resource, ['items.results'])
Related
Project in Laravel (9), and PHP (8.1).
I want to import an excel file and use maatwebsite/excel (3.1) package.
I can import a file, and save the file into the model, like this:
import class:
class BankTransfersHistoryImport implements ToModel, WithHeadingRow, WithValidation
{
use Importable;
/**
* #param array $row
*
* #return \Illuminate\Database\Eloquent\Model|null
*/
public function model(array $row)
{
return new BankTransfersHistory([
'loanId' => $row['loanId'],
'actionDate' => transformDate($row['actionDate']),
'worth' => $row['worth'],
.
.
]);
}
public function headingRow(): int
{
return 2;
}
public function rules(): array
{
return [
'*.loanId' => ['required', 'numeric'],
... some roles ...
];
}
}
controller:
$import = new BankTransfersHistoryImport;
try {
// date validation
$collection = $import->toCollection($file);
... some validation about the date ...
$import->import($file);
... check and update rows ...
return [
"message" => some message,
"data" => [
some data
],
];
} catch (\Maatwebsite\Excel\Validators\ValidationException$e) {
$failures = $e->failures();
foreach ($failures as $failure) {
$failure->row(); // row that went wrong
$failure->attribute(); // either heading key (if using heading row concern) or column index
$failure->errors(); // Actual error messages from Laravel validator
$failure->values(); // The values of the row that has failed.
}
return $failures;
}
My question is:
If I can get the response of the file after saving the data, and that will give me the data with the id of the row that was saved.
In some cases, I will have to update a row. That's why I would like to get the ID.
Now in the check and update rows section, I update by loanId + actionDate. I want it to be done by ID.
something like this:
code:
$data = $import->import($file);
data will be like:
[
{
"id": 1,
"loanId": 21001,
"actionDate": "2020-01-02T00:00:00.000000Z",
"worth": 2997.09,
"offerId": 1,
},
{
"id": 2,
"loanId": 21002,
"actionDate": "2020-01-02T00:00:00.000000Z",
"worth": 3000,
"offerId": 10,
},
]
You can create a function on import class which will return the imported data, adding a sample for your reference.
UsersImport.php
<?php
namespace App\Imports;
use App\Models\User;
use Maatwebsite\Excel\Concerns\ToModel;
class UsersImport implements ToModel
{
private $rows;
public function __construct() {
$this->rows = collect();
}
/**
* #param array $row
*
* #return User|null
*/
public function model(array $row)
{
$user = new User([
'name' => $row[0],
'email' => $row[1],
'password' => bcrypt(12345678),
]);
$this->rows->push($user);
return $user;
}
/**
* Returns Imported Data
*
* #return \Illuminate\Support\Collection
*/
public function getImportedData(): \Illuminate\Support\Collection
{
return $this->rows;
}
}
Your Import Function in Controller
public function import(UsersImport $usersImport)
{
Excel::import($usersImport, public_path('users.xlsx'));
$usersImport->getImportedData();
}
I have a find() method in my if else statement that queries the database and returns the data as an array. The if part works fine. The problem is in the else part. When I try to access the index interface in the browser, am getting this error.
Unable to locate an object compatible with paginate.
RuntimeException
From what I have gathered so far, the paginate() method works with objects not arrays. Am stuck on how to come to my desired outcome. Am new to CakePHP, a not so advanced/complicated response would be appreciated. Thanks
/**
* Assets Controller
*
*
* #method \App\Model\Entity\Asset[] paginate($object = null, array $settings = [])
*/
class AssetsController extends AppController
{
/**
* Index method
*
* #return \Cake\Http\Response|void
*/
public function index()
{
$this->loadModel('Users');
$username = $this->request->session()->read('Auth.User.username');
$userdetail = $this->Users->find('all')->where(['username' => $username])->first();
$school = $userdetail->school_unit;
$roleid = $userdetail->role_id;
if ($roleid == 1) {
$this->paginate = [
'contain' => ['SchoolUnits', 'AssetConditions', 'AssetCategories', 'AssetGroups', 'AssetStatus']
];
$assets = $this->paginate($this->Assets);
$this->set(compact('assets'));
$this->set('_serialize', ['assets']);
} else {
$results = $this->Assets->find('all')->contain(['SchoolUnits', 'AssetConditions', 'AssetCategories', 'AssetGroups', 'AssetStatus'])->where(['school_unit_id' => $school])->first();
$assets = $this->paginate($this->$results);
$this->set(compact('assets'));
$this->set('_serialize', ['assets']);
}
}
I've been trying for countless hours now, but still having issues updating a models relationship, the closest I've got to is a 'Method fill does not exist.' error.
Listing model:
class Listing extends Model
{
protected $fillable = [
'uid', 'start_date',...........
];
public function locations()
{
return $this->hasMany('App\ListingLocation');
}
}
Location (relationship to listing - hasMany):
class ListingLocation extends Model
{
protected $fillable = [
'listing_id', 'location',
];
public function listing()
{
return $this->belongsTo('App\Listing');
}
}
This returns my model and relationship, which I can view with dd($listing)
$listing = Listing::with('locations')->findOrFail($id);
This will update my listing model, which I can see the changes after calling dd($listing) again
$listing->fill($array);
However when I attempt to fill the relationship as per below, I get 'Method fill does not exist.'
$listing->locations->fill($array['locations']);
How can I update the relationship successfully before calling $listing->push();?
Change your location to a single record, not a collection
For example:
$listings->locations->first()->fill($array['locations']);
to fill every record use foreach
#foreach($listings->locations as $location)
$location->fill(do_something);
#endforeach
I ended up creating a new class to extend hasMany which allowed me to use sync as per alexweissman at https://laracasts.com/discuss/channels/general-discussion/syncing-one-to-many-relationships.
Extract from forum:
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* #link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Database/Eloquent/Relations/HasMany.php
*/
class HasManySyncable extends HasMany
{
public function sync($data, $deleting = true)
{
$changes = [
'created' => [], 'deleted' => [], 'updated' => [],
];
$relatedKeyName = $this->related->getKeyName();
// First we need to attach any of the associated models that are not currently
// in the child entity table. We'll spin through the given IDs, checking to see
// if they exist in the array of current ones, and if not we will insert.
$current = $this->newQuery()->pluck(
$relatedKeyName
)->all();
// Separate the submitted data into "update" and "new"
$updateRows = [];
$newRows = [];
foreach ($data as $row) {
// We determine "updateable" rows as those whose $relatedKeyName (usually 'id') is set, not empty, and
// match a related row in the database.
if (isset($row[$relatedKeyName]) && !empty($row[$relatedKeyName]) && in_array($row[$relatedKeyName], $current)) {
$id = $row[$relatedKeyName];
$updateRows[$id] = $row;
} else {
$newRows[] = $row;
}
}
// Next, we'll determine the rows in the database that aren't in the "update" list.
// These rows will be scheduled for deletion. Again, we determine based on the relatedKeyName (typically 'id').
$updateIds = array_keys($updateRows);
$deleteIds = [];
foreach ($current as $currentId) {
if (!in_array($currentId, $updateIds)) {
$deleteIds[] = $currentId;
}
}
// Delete any non-matching rows
if ($deleting && count($deleteIds) > 0) {
$this->getRelated()->destroy($deleteIds);
$changes['deleted'] = $this->castKeys($deleteIds);
}
// Update the updatable rows
foreach ($updateRows as $id => $row) {
$this->getRelated()->where($relatedKeyName, $id)
->update($row);
}
$changes['updated'] = $this->castKeys($updateIds);
// Insert the new rows
$newIds = [];
foreach ($newRows as $row) {
$newModel = $this->create($row);
$newIds[] = $newModel->$relatedKeyName;
}
$changes['created'][] = $this->castKeys($newIds);
return $changes;
}
/**
* Cast the given keys to integers if they are numeric and string otherwise.
*
* #param array $keys
* #return array
*/
protected function castKeys(array $keys)
{
return (array) array_map(function ($v) {
return $this->castKey($v);
}, $keys);
}
/**
* Cast the given key to an integer if it is numeric.
*
* #param mixed $key
* #return mixed
*/
protected function castKey($key)
{
return is_numeric($key) ? (int) $key : (string) $key;
}
}
You can then override Eloquent's hasMany method in your model class:
/**
* Overrides the default Eloquent hasMany relationship to return a HasManySyncable.
*
* {#inheritDoc}
*/
public function hasMany($related, $foreignKey = null, $localKey = null)
{
$instance = $this->newRelatedInstance($related);
$foreignKey = $foreignKey ?: $this->getForeignKey();
$localKey = $localKey ?: $this->getKeyName();
return new HasManySyncable(
$instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey
);
}
/**
* Get all of a user's phone numbers.
*/
public function phones()
{
return $this->hasMany('App\Phone');
}
A sync method will now be available to any hasMany relationships you have on this model:
$user->phones()->sync([
[
'id' => 21,
'label' => "primary",
'number' => "5555551212"
],
[
'id' => null,
'label' => "mobile",
'number' => "1112223333"
]
]);
I'm working with PageKit CMS. I have 2 tables with Many To Many relation (item and type).
Item model:
class Item implements \JsonSerializable
{
...
/**
* #ManyToMany(targetEntity="Type", tableThrough="#prefix_item_type", keyThroughFrom="item_id", keyThroughTo="type_id")
*/
public $types;
...
}
Type model:
class Type implements \JsonSerializable
{
...
/**
* #ManyToMany(targetEntity="Item", tableThrough="#prefix_item_type", keyThroughFrom="type_id", keyThroughTo="item_id")
*/
public $items;
...
}
In backend interface on item edit page I created multi select with all types. When I send item save request, I get type ids.
My save item method have a look:
public function saveAction($data, $id = 0, $selected_types = [])
{
/*
* $selected_types = array(2) {
* [0]=>int(1)
* [1]=>int(2)
* }
*/
if (!$id || !$item = Item::query()->related(['types'])) {
if ($id) {
App::abort(404, __('Item not found'));
}
$item = Item::create();
}
if (!$data['slug'] = App::filter($data['slug'] ?: $data['title'], 'slugify')) {
App::abort(400, __('Invalid alias'));
}
if(!App::user()->hasAccess('ext_name: manage all items')) {
$data['user_id'] = App::user()->id;
}
if(!App::user()->hasAccess('ext_name: manage all items') && !App::user()->hasAccess('ext_name: manage own items') && $item->user_id !== App::user()->id) {
App::abort(403, __('Access denied'));
}
$item->save($data);
/*
* Here I need to sync $item->types with $selected_types ids
*/
return [
'message' => 'success',
'entity' => $item,
];
}
How can I sync this relation if I have current item id and new type ids?
I'm building a client application that needs to have the ids of related models in my server API response.
In my example I've got two models, a Post and a Tag model. The relationship between them is many-to-many, so a pivot table is required.
class Post extends Eloquent {
protected $fillable = [ 'title', 'body' ];
public function tags()
{
return $this->hasMany('Tag');
}
}
class Tag extends Eloquent {
protected $fillable = [ 'title' ];
public function posts()
{
return $this->belongsToMany('Post');
}
}
I've got a resourcefull controller set up on the /api/posts route like this:
class PostsController extends \BaseController {
public function index()
{
$posts = Post::all();
return Response::json([ 'posts' => $posts->toArray() ]);
}
}
This will return a response much like this one:
{
"posts": [
{
"title": "Laravel is awesome",
"body": "Lorem Ipsum..."
},
{
"title": "Did I mention how awesome Laravel is?",
"body": "Lorem Ipsum..."
}
]
}
What I'm looking for is an easy way to include the ids of the related Tags model in the response like this:
{
"posts": [
{
"title": "Laravel is awesome",
"body": "Lorem Ipsum...",
"tags": [ 1, 2, 3 ]
},
{
"title": "Did I mention how awesome Laravel is?",
"body": "Lorem Ipsum...",
"tags": [ 1, 2, 4 ]
}
]
}
This isn't the most elegant solution, but it may work like you want (code not tested)
public function index()
{
$posts = Post::all();
$postsArray = array();
foreach ($posts as $post)
{
$postArray = $post->toArray();
$postArray['tags'] = array_values($post->tags->lists('id'));
$postsArray[] = $postArray;
}
return Response::json([ 'posts' => $postsArray]);
}
Add the following code to your Model/BaseModel:
/**
* Set additional attributes as hidden on the current Model
*
* #return instanceof Model
*/
public function addHidden($attribute)
{
$hidden = $this->getHidden();
array_push($hidden, $attribute);
$this->setHidden($hidden);
// Make method chainable
return $this;
}
/**
* Convert appended collections into a list of attributes
*
* #param object $data Model OR Collection
* #param string|array $levels Levels to iterate over
* #param string $attribute The attribute we want to get listified
* #param boolean $hideOrigin Hide the original relationship data from the result set
* #return Model
*/
public function listAttributes($data, $levels, $attribute = 'id', $hideOrigin = true)
{
// Set some defaults on first call of this function (because this function is recursive)
if (! is_array($levels))
$levels = explode('.', $levels);
if ($data instanceof Illuminate\Database\Eloquent\Collection) // Collection of Model objects
{
// We are dealing with an array here, so iterate over its contents and use recursion to look deeper:
foreach ($data as $row)
{
$this->listAttributes($row, $levels, $attribute, $hideOrigin);
}
}
else
{
// Fetch the name of the current level we are looking at
$curLevel = array_shift($levels);
if (is_object($data->{$curLevel}))
{
if (! empty($levels))
{
// We are traversing the right section, but are not at the level of the list yet... Let's use recursion to look deeper:
$this->listAttributes($data->{$curLevel}, $levels, $attribute, $hideOrigin);
}
else
{
// Hide the appended collection itself from the result set, if the user didn't request it
if ($hideOrigin)
$data->addHidden($curLevel);
// Convert Collection to Eloquent lists()
if (is_array($attribute)) // Use specific attributes as key and value
$data->{$curLevel . '_' . $attribute[0]} = $data->{$curLevel}->lists($attribute[0], $attribute[1]);
else // Use specific attribute as value (= numeric keys)
$data->{$curLevel . '_' . $attribute} = $data->{$curLevel}->lists($attribute);
}
}
}
return $data;
}
You can use it on your Model/Collection Object like this:
// Fetch posts data
$data = Post::with('tags')->get(); // or use ->first()
// Convert relationship data to list of id's
$data->listAttributes($data, 'tags');
$data will now contain the following object store:
{
"posts": [
{
"title": "Laravel is awesome",
"body": "Lorem Ipsum...",
"tags_id": [ 1, 2, 3 ]
},
{
"title": "Did I mention how awesome Laravel is?",
"body": "Lorem Ipsum...",
"tags_id": [ 1, 2, 4 ]
}
]
}
It also supports nested relationships:
// Fetch posts data
$data = Post::with('comments', 'comments.tags')->get(); // or use ->first()
// Convert relationship data to list of id's
$data->listAttributes($data, 'comments.tags');
Eloquent relationship returns a collection object which you can filter or even modify, say if you need only an array of the id's you can do this:
$posts->pluck('id');
$posts->pluck('id')->toArray();
I think you need to use the $appends property. Take a look at the docs here http://laravel.com/docs/eloquent#converting-to-arrays-or-json.
This question also is relevant as he encountered the same issue as you (see the edit on the accepted answer).
Add a custom attribute to a Laravel / Eloquent model on load?