Is it possible to add non-persistent attributes to Laravel models? - php

I'm creating an API with Laravel (Lumen) in which there are objects which contain a field which is a path to a file.
These paths are stored as relative paths in the database but upon returning them to user I have to convert them to absolute urls.
Now I was wondering if there is a convenient way to add a non-persistent field to the model objects. Obviously there are Mutators but they are persisted to the database.
I also have thought of creating an after-middleware which traverses the object tree and converts every path field it finds but this is not an elegant way.
Here is the final transformation I need:
[
{
"id": 1,
"title": "Some title",
"media": [
{
"id": 435,
"path": "relative/path/to/some/file.ext"
},
{
"id": 436,
"path": "relative/path/to/some/file2.ext"
}
]
}
]
To:
[
{
"id": 1,
"title": "Some title",
"media": [
{
"id": 435,
"url": "http://example.com/relative/path/to/some/file.ext"
},
{
"id": 436,
"url": "http://example.com/relative/path/to/some/file2.ext"
}
]
}
]
Any idea is welcome.

You can use Laravel accessors,
From the Docs:
The original value of the column is passed to the accessor, allowing
you to manipulate and return the value.
These are not persisted in the DB but are modified as and when you access them.
For example:
class User extends Model
{
/**
* Get the user's first name.
*
* #param string $value
* #return string
*/
public function getFirstNameAttribute($value)
{
return ucfirst($value);
}
}
Usage:
$user = App\User::find(1);
$firstName = $user->first_name;
In your use case:
Define an accessor in the Media Model for path attribute.
public function getPathAttribute($value)
{
return storage_path($value);
}
If you need to access the property with a different name (alias):
public function getAliasAttribute()
{
return storage_path($this->attributes['path']);
}
// $model->alias

As #Sapnesh Naik said what you need is a simple accessor like this:
public function getPathAttribute($value)
{
$url = config('relative_url') or env('PATH') or $sthElse;
return $url.$this->attributes['path'];
}

Related

Appended one accessor but resource has more than one, how is this possible? - Laravel

I am trying to use an accessor on a model to return the status whether a relationship exists.
My User model:
class User {
protected $appends = ['has_profile'];
public function profile()
{
return $this->hasOne(Profile::class)
}
public function getHasProfileAttribute()
{
$exists = $this->profile;
if($exists){
return 1;
}
else{
return 0;
}
}
}
The problem is when the User model is loaded via User::find(1)->get();, the profile property is also loaded into JSON resource whereas, I only want the has_profile attribute in my JSON return. How should I query the relationship existence without loading it, or should I unload the relationship?
What I Get
"data": {
"id": 270,
"name": "John Doe",
"mobile_number": "01234567890",
"created_at": "2021-08-19T06:55:33.000000Z",
"updated_at": "2021-08-19T06:55:33.000000Z",
"deleted_at": null,
"has_profile": 1,
"profile": {
"id": 1,
"details": "Details"
}
}
What I want
"data": {
"id": 270,
"name": "John Doe"
"mobile_number": "01234567890",
"created_at": "2021-08-19T06:55:33.000000Z",
"updated_at": "2021-08-19T06:55:33.000000Z",
"deleted_at": null,
"has_profile": 1
}
Updated Solution
The problem was $this->profile which led to the profile relation being attached. When used as $this->profile()->get(); or $this->profile()->first(); it works as expected.
You can use unset to remove the attribute profile.
public function getHasProfileAttribute()
{
$exists = $this->profile;
unset($this->profile);
if($exists){
return 1;
}
else{
return 0;
}
}
You can use the except() method from the documentation
User::find(1)->get()->except('profile');
Maybe you have to change order i can't test right now but it's the idea

Laravel 7.0 pagination not working in method chain

Here i am using method chaining on Laravel Eloquent model (User) with pagination,but after calling method each pagination stop working.
Is this behaviour expected or i am missing something. there noting mentioned about this on official docs.
Works fine
User::paginate(10)
->appends(request()->all());
Output
{
"data": [
{
"id": 1,
"email": "user#email.com",
},
{
"id": 2,
"email": "two#email.com",
},
],
"current_page": 1,
"first_page_url": "//localhost/users?page=1",
"last_page": 5,
"last_page_url": "//localhost/users?page=5",
"next_page_url": "//localhost/users?page=2",
"path": "//localhost/users",
"per_page": 10,
"prev_page_url": null,
"total": 50
}
But problem arrives when i call each() method on it
Not working
User::paginate(10)
->appends(request()->all())
->each(function ($user) {
$user['someAttribute'] = 'value';
return $user;
})
Output (pagination not working)
plain simple result only query records. (Omitted pagination info)
[
{
"id": 1,
"email": "user#email.com",
},
{
"id": 2,
"email": "two#email.com",
},
]
I don't think you can retrieve the pagination properties after altering the $items. You'd have to convert the altered data into the LengthAwarePaginator object manually.
$users = User::paginate(15);
$alteredUsers = $users->getCollection()
->each(function($user) {
$user['someAttribute'] = 'value';
return $user;
});
$newPaginatedUsers = new \Illuminate\Pagination\LengthAwarePaginator(
$alteredUsers,
$users->total(),
$users->perPage(),
$users->currentPage(),
[
'path' => \Request::URL(), // optional
]
)->appends(request()->all());
You can look at the source here and have a better idea of how to build the object.
Create an accessor:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public function getSomeAttributeAttribute()
{
return 'value';
}
}
$users = User::paginate(10)->appends(request()->all());
https://laravel.com/docs/8.x/eloquent-mutators#defining-an-accessor
https://laravel.com/docs/8.x/eloquent-serialization#appending-values-to-json

Symfony 4 & Doctrine 2 serialize after removing (first) item from collection causes converting to JSON object instead of array

I'm having a lot of trouble with serializing collection from which I've removed first element.
I have CompaniesCollection entity with Many2Many relation to Company entity.
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Company")
* #Groups({"get-by-collection-owner"})
*/
private $items;
When I fetch the object with that collection, I receive items as an array of two elements I've added in the first place. I serialize it
{
"id": 19,
"name": "collection dummy name",
"items": [
{
"id": 431,
"name": "Company 1"
},
{
"id": 435,
"name": "Company 2"
}
],
"createdAt": "2019-03-11T13:55:43+01:00",
"updatedAt": "2019-03-11T15:48:57+01:00"
},
Then I remove FIRST item:
$collection->removeItem($companyToRemove);
$em = $this->getDoctrine()->getManager();
$em->persist($collection);
$em->persist($companyToRemove);
$em->flush();
$results = $companiesCollectionRepository->getCollections($companyLoader->getCurrentCompany());
Serialize the $results and get not an array of one element, but object with key of second element from the previous array of items:
{
"id": 19,
"name": "collection dummy name",
"items": {
"1": {
"id": 435,
"name": "Company 2"
}
},
"createdAt": "2019-03-11T13:55:43+01:00",
"updatedAt": "2019-03-11T15:52:48+01:00"
},
When I reload the page and fetch this object, the collection is again one element array, not an object. Apparently Doctrine doesn't get new results from database, but returns data which has already loaded in the memory.
And serializer most likely treats this "array" as an "object", because it doesn't start with 0 for a key of first array element, but with key 1.
Is there any way to make that query again, so I get freshly generated keys, or refresh these keys?
EDIT:
Actually I've finally found simple solution for this: refresh after flush
$collection->removeItem($companyToRemove);
$em = $this->getDoctrine()->getManager();
$em->persist($collection);
$em->persist($companyToRemove);
$em->flush();
$em->refresh($collection);
I've faced the same problem. And here is another solution which seems working for me.
Custom normalizer, which will be used for collections with "holes" in they keys:
namespace App\Serializer\Normalizer;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\SerializerAwareInterface;
use Symfony\Component\Serializer\SerializerAwareTrait;
class DoctrineCollectionNormalizer implements NormalizerInterface, SerializerAwareInterface
{
use SerializerAwareTrait;
public function normalize($object, $format = null, array $context = array()): array
{
/* #var $object Collection */
$values = $object->getValues();
$object->clear();
foreach ($values as $k => $value) {
$object->set($k, $value);
}
return $this->serializer->normalize($object, $format, $context);
}
public function supportsNormalization($data, $format = null): bool
{
if ($data instanceof Collection) {
$keys = $data->getKeys();
$count = count($keys);
$lastKey = (int) array_pop($keys);
return $count && $lastKey !== $count-1;
}
return false;
}
}

Create an object / model out of JSON data, that is not an exact copy of the wanted model-object

How would one in an OOP-way "hydrate", "map", "create" (or whatever it's called) an object or model representation of this JSON data. The JSON string I've included below is the raw data received from Google's AdWords API and is not an exact copy of the structure I want and need, and I may therefor not just cast it to an object and similar.
I've been looking at various mapping libraries and hydration libraries but I cannot seem to wrap my head around how to get it to work properly.
{
"report-name": {
"#name": "Criteria performance report #5b3e67189ac85",
},
"date-range": {
"#date": "Jun 5, 2018-Jul 4, 2018",
},
"table": {
"columns": {
"column": [
{
"#name": "adGroup",
"#display": "Ad group",
},
{
"#name": "impressions",
"#display": "Impressions",
},
{
"#name": "clicks",
"#display": "Clicks",
},
{
"#name": "cost",
"#display": "Cost",
}
]
},
"row": [
{
"#adGroup": "Ad Group 1 of Another Campaign",
"#impressions": 0,
"#clicks": 0,
"#cost": 0,
},
{
"#adGroup": "Ad group 1 of Initial Campaign",
"#impressions": 0,
"#clicks": 0,
"#cost": 0,
}
]
}
}
I would imagine the object or model looking kind of like this.
There are different types of reports, but the structure look exactly the same, just the items are not containing the same attributes. The posted JSON structure represents an "AdGroup", meaning the "rows" would need to get de-serialized into different models. Basically calling "getRowItems" would need to return AdGroup-object's for the posted JSON, but may need to return other types of objects for different types of report data.
class ReportModelExample {
private $reportName;
private $reportDate;
private $reportItems;
public function getReportName()
{
return $this->reportName;
}
public function setReportName($reportName): void
{
$this->reportName = $reportName;
}
public function getReportDate()
{
return $this->reportDate;
}
public function setReportDate($reportDate): void
{
$this->reportDate = $reportDate;
}
public function getReportItems()
{
return $this->reportItems;
}
public function setReportItems($reportItems): void
{
$this->reportItems = $reportItems;
}
}

Force JMS Serialiser to output an object keyed by a specific field

I have an entity Product with a one-to-many relationship to an entity Property. When I serialise a product instance using the JMS Serialiser I get the following JSON output:
{
"id": 123,
"name": "Mankini Thong",
"properties": [{
"label": "Minimal size",
"name": "min_size",
"value": "S"
}, {
"label": "Maximum size",
"name": "max_size",
"value": "XXXL"
}, {
"label": "colour",
"name": "Colour",
"value": "Office Green"
}]
}
I try to get the serialiser to serialise the properties collection as an object in which a certain field is used as key. For instance, the name field. The desired output is:
{
"id": 123,
"name": "Mankini Thong",
"properties": {
"min_size": {
"label": "Minimal size",
"value": "S"
},
"max_size": {
"label": "Maximum size",
"value": "XXXL"
},
"colour": {
"label": "Colour",
"value": "Office Green"
}
}
}
What would be the best approach to achieve this?
Ok, I figured it out:
First add a virtual property to the serialisation mapping and exclude the original properties field. My configuration is in yaml but using annotations shouldn't be that different:
properties:
properties:
exclude: true
virtual_properties:
getKeyedProperties:
serialized_name: properties
type: array<Foo\BarBundle\Document\Property>
Then I've added the getKeyedProperties method to the document class in Foo\BarBundle\Document\Article:
/**
* Get properties keyed by name
*
* Use the following annotations in case you defined your mapping using
* annotations instead of a Yaml or Xml file:
*
* #Serializer\VirtualProperty
* #Serializer\SerializedName("properties")
*
* #return array
*/
public function getKeyedProperties()
{
$results = [];
foreach ($this->getProperties() as $property) {
$results[$property->getName()] = $property;
}
return $results;
}
Now, the serialised output contains an object properties which are serialised article properties keyed by name.

Categories