pro's, amateurs and php enthousiasts.
I am working on a Laravel task wicht envolved dynamic data, collections and graphs.
In order to see what is wrong i kinda need some help, since I can't see it clearly anymore. I should pause and work on something else but this is a bottleneck for me.
I have a collection called orders.
in those orders I have grouped them by date. So far so good. Example below is a die and dump.
Exactly what i need in this stage.
"2022-01-29" => Illuminate\Support\Collection {#4397 ▶}
Now comes the mweh part.
I have a class called Datahandler
in that class I have three methods in it
simplified version of it:
Abstract Class DataHandler
{
/**
* Handles the conversion to dataset for the chart
*
* #param string $label
* #return void
*/
public function handle(string $label):void
{
$this->chart->addDataset($this->process->map(
$this->bind([$this, 'dataLogic'])
)->toArray()
, $label
);
}
/**
* Binds callbacks for the Handler of the class
*
* #param array $callable
* #return Closure
*/
function bind(array $callable): Closure
{
return function () use ($callable) {
call_user_func_array($callable, func_get_args());
};
}
/**
* Defines the fields I need to return to the collection
*
* #param Collection $group
* #return array
*/
#[Pure] #[ArrayShape(['total' => "int"])]
protected function dataLogic(Collection $group): array
{
return [
'total' => $group->count()
];
}
}
So in the handle function you can see I am binding ($this->bind()) my $this->process (collection data) to a callback ( $this->dataLogic() ). The protected function dataLogic is protected because every child of this Abstract class needs to have it's own logic in there.
so this function is being executed from within the parent, this is good cause it should be the default behaviour unless the child has the same function. If i do a var_dump on $group in method dataLogic I also have the correct value and the $group->count() also presents the corrent count of said data.
however the return is null. I am not so well trained in the use of callbacks, has anyone an idea on what is going wrong or even a better solution then the one I am trying to create?
forgot to mention the result of my code:
"2022-01-29" => null
It should be
"2022-01-29" => 30
Kind Regards,
Marcel
I solved it by doing the following.
I completely removed the bind function and handled my function as a callable for it got the needed solution, is there a better one, sure there is somewhere so any ideas are still welcome, but for now i can continue further.
Abstract Class DataHandler
{
/**
* Handles the conversion to dataset for the chart
*
* #param string $label
* #return void
*/
public function handle(string $label):void
{
$this->chart->addDataset($this->process->map($this->dataLogic()
)->toArray()
, $label
);
}
/**
* Defines the fields I need to return to the collection
*
* #return array
*/
#[Pure] #[ArrayShape(['total' => "int"])]
protected function dataLogic(): callable
{
return function ($group) {
return $group->count();
};
}
}
I am currently working on a Laravel 5.8 project and when updating a model noticed that even though there aren't any changes to the model I'm saving the same model back into the database.
My thinking to avoid this was the following:
$model = Model::find($id);
$model->fill([
"name" => $request->name,
...
]);
if($model->isDirty){
$model->save()
}
Problem is that even though I don't change values in my model I'm still entering the if() condition and saving the model. I tried using a temp variable and debugged $model->getChanges() and I get an empty array.
Is this expected behavior?
There is a difference yes.
Related code:
https://github.com/laravel/framework/blob/6.x/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php#L1060
isDirty code
/**
* Determine if the model or any of the given attribute(s) have been modified.
*
* #param array|string|null $attributes
* #return bool
*/
public function isDirty($attributes = null)
{
return $this->hasChanges(
$this->getDirty(), is_array($attributes) ? $attributes : func_get_args()
);
}
getChanges() & getDirty code
/**
* Get the attributes that have been changed since last sync.
*
* #return array
*/
public function getDirty()
{
$dirty = [];
foreach ($this->getAttributes() as $key => $value) {
if (! $this->originalIsEquivalent($key, $value)) {
$dirty[$key] = $value;
}
}
return $dirty;
}
/**
* Get the attributes that were changed.
*
* #return array
*/
public function getChanges()
{
return $this->changes;
}
To summarize.
Answer used from this post: https://laracasts.com/discuss/channels/eloquent/observer-column-update-isdirty-or-waschanged
isDirty (and getDirty) is used BEFORE save, to see what attributes
were changed between the time when it was retrieved from the database
and the time of the call, while wasChanged (and getChanges) is used
AFTER save, to see that attributes were changed/updated in the last
save (from code to the database).
The reason you get in the isDirty check is that before the check you do a fill(). I think this will auto-fill updated_at. So, in fact, the model, in this case, has been changed.
I have an API that uses API resource and resource collections to correctly format the JSON responses. In order to decouple my controller from my model I use an adapter to query the underlying model. I'd like to pass the adapter return values as arrays, rather than Eloquent models, to ensure that any furture adapters are easier to right in respect to their return data structures. To create the array return values I serialise my adapter Eloquent results with ->toArray().
I have 2 API Resources to correctly format these results, for a single resource I have:
use Illuminate\Http\Resources\Json\Resource;
class Todo extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return $this->resource;
}
}
For a resource collection I have:
use Illuminate\Http\Resources\Json\ResourceCollection;
class TodoCollection extends ResourceCollection
{
/**
* Transform the resource collection into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'data' => $this->collection
->map
->toArray($request)
->all()
];
}
}
When I return a single resource from my controller with :
use App\Http\Resources\Todo;
public function show($id)
{
return new Todo($this->todoAdapter->findById($id));
}
and the adapter query as:
public function findById(int $id){
return TodoModel::findOrFail($id)
->toArray();
}
This works as expected. The problem comes when I try to pass an array of a collection of models i.e.
public function index(Request $request)
{
$todos = $this->todoAdapter->getAllForUserId(Auth::id(), 'created_by', 'desc', self::DEFAULT_PAGINATE);
return new TodoCollection($todos);
}
and the adapter query as:
public function getAllForUserId(int $userId, string $sortField, string $sortDir, int $pageSize = self::DEFAULT_PAGINATE)
{
return Todo::BelongsUser($userId)
->orderBy($sortField, $sortDir)
->paginate($pageSize)
->toArray();
}
I get the following error:
"message": "Call to a member function first() on array",
"exception": "Symfony\\Component\\Debug\\Exception\\FatalThrowableError",
"file": "/home/vagrant/code/public/umotif/vendor/laravel/framework/src/Illuminate/Http/Resources/CollectsResources.php",
"line": 24,
I'm guessing that I can't do 'new TodoCollection($todos)' where $todos is an array of results. How would I get my todoCollection to work with arrays? Any suggestions would be much appreciated!
Your collections toArray is trying to do too much:
$this->collection
->map
->toArray($request)
->all()
Just directly call $this->collection->toArray().
Just to update this. In the end I found that creating a collection from the array of results and passing that to the resource collection constructor worked, though I did have to add explicit mappings within the resource collection for links and meta etc.
I've got a model and the model its self could be linked to multiple other databases but only one at a time.
Instead of having a eloquent method for all the possible databases; it could have one that will use a variable from the self instance to choose the database and return just that.
It will save alot of work, as returning each one and testing to see if there are any results is cumbersome.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Feature extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'companies';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name',
];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = [
'db_name',
'enabled',
];
/**
* Uses the its own database name to determine which input to return.
*/
public function inputs() {
// if this->hidden->db_name == 'input type 1'
// return $this->HasMany(InputType1::class);
.... and so on
} // end function inputs
}
This is definitely a strange behaviour but I think you can achieve what you are looking for like so :
//in your model
public function inputs()
{
switch ($this->attributes['db_name']) {
case : 'input type 1':
return $this->hasMany(InputType1::class);
case : //some other database name
return //another relation
}
}
Expanding on shempognon answer, what I actually got to work was
switch($this->db_name) {
case 'Input_Timesheet':
return $this->hasMany(Input_type1::class);
}
I'm developing game app and using Symfony 2.0. I have many AJAX requests to the backend. And more responses is converting entity to JSON. For example:
class DefaultController extends Controller
{
public function launchAction()
{
$user = $this->getDoctrine()
->getRepository('UserBundle:User')
->find($id);
// encode user to json format
$userDataAsJson = $this->encodeUserDataToJson($user);
return array(
'userDataAsJson' => $userDataAsJson
);
}
private function encodeUserDataToJson(User $user)
{
$userData = array(
'id' => $user->getId(),
'profile' => array(
'nickname' => $user->getProfile()->getNickname()
)
);
$jsonEncoder = new JsonEncoder();
return $jsonEncoder->encode($userData, $format = 'json');
}
}
And all my controllers do the same thing: get an entity and encode some of its fields to JSON. I know that I can use normalizers and encode all entitities. But what if an entity has cycled links to other entity? Or the entities graph is very big? Do you have any suggestions?
I think about some encoding schema for entities... or using NormalizableInterface to avoid cycling..,
With php5.4 now you can do :
use JsonSerializable;
/**
* #Entity(repositoryClass="App\Entity\User")
* #Table(name="user")
*/
class MyUserEntity implements JsonSerializable
{
/** #Column(length=50) */
private $name;
/** #Column(length=50) */
private $login;
public function jsonSerialize()
{
return array(
'name' => $this->name,
'login'=> $this->login,
);
}
}
And then call
json_encode(MyUserEntity);
Another option is to use the JMSSerializerBundle. In your controller you then do
$serializer = $this->container->get('serializer');
$reports = $serializer->serialize($doctrineobject, 'json');
return new Response($reports); // should be $reports as $doctrineobject is not serialized
You can configure how the serialization is done by using annotations in the entity class. See the documentation in the link above. For example, here's how you would exclude linked entities:
/**
* Iddp\RorBundle\Entity\Report
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="Iddp\RorBundle\Entity\ReportRepository")
* #ExclusionPolicy("None")
*/
....
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="reports")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id")
* #Exclude
*/
protected $client;
You can automatically encode into Json, your complex entity with:
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
$serializer = new Serializer(array(new GetSetMethodNormalizer()), array('json' => new
JsonEncoder()));
$json = $serializer->serialize($entity, 'json');
To complete the answer: Symfony2 comes with a wrapper around json_encode:
Symfony/Component/HttpFoundation/JsonResponse
Typical usage in your Controllers:
...
use Symfony\Component\HttpFoundation\JsonResponse;
...
public function acmeAction() {
...
return new JsonResponse($array);
}
I found the solution to the problem of serializing entities was as follows:
#config/config.yml
services:
serializer.method:
class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
serializer.encoder.json:
class: Symfony\Component\Serializer\Encoder\JsonEncoder
serializer:
class: Symfony\Component\Serializer\Serializer
arguments:
- [#serializer.method]
- {json: #serializer.encoder.json }
in my controller:
$serializer = $this->get('serializer');
$entity = $this->get('doctrine')
->getRepository('myBundle:Entity')
->findOneBy($params);
$collection = $this->get('doctrine')
->getRepository('myBundle:Entity')
->findBy($params);
$toEncode = array(
'response' => array(
'entity' => $serializer->normalize($entity),
'entities' => $serializer->normalize($collection)
),
);
return new Response(json_encode($toEncode));
other example:
$serializer = $this->get('serializer');
$collection = $this->get('doctrine')
->getRepository('myBundle:Entity')
->findBy($params);
$json = $serializer->serialize($collection, 'json');
return new Response($json);
you can even configure it to deserialize arrays in http://api.symfony.com/2.0
I just had to solve the same problem: json-encoding an entity ("User") having a One-To-Many Bidirectional Association to another Entity ("Location").
I tried several things and I think now I found the best acceptable solution. The idea was to use the same code as written by David, but somehow intercept the infinite recursion by telling the Normalizer to stop at some point.
I did not want to implement a custom normalizer, as this GetSetMethodNormalizer is a nice approach in my opinion (based on reflection etc.). So I've decided to subclass it, which is not trivial at first sight, because the method to say if to include a property (isGetMethod) is private.
But, one could override the normalize method, so I intercepted at this point, by simply unsetting the property that references "Location" - so the inifinite loop is interrupted.
In code it looks like this:
class GetSetMethodNormalizer extends \Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer {
public function normalize($object, $format = null)
{
// if the object is a User, unset location for normalization, without touching the original object
if($object instanceof \Leonex\MoveBundle\Entity\User) {
$object = clone $object;
$object->setLocations(new \Doctrine\Common\Collections\ArrayCollection());
}
return parent::normalize($object, $format);
}
}
I had the same problem and I chosed to create my own encoder, which will cope by themself with recursion.
I created classes which implements Symfony\Component\Serializer\Normalizer\NormalizerInterface, and a service which holds every NormalizerInterface.
#This is the NormalizerService
class NormalizerService
{
//normalizer are stored in private properties
private $entityOneNormalizer;
private $entityTwoNormalizer;
public function getEntityOneNormalizer()
{
//Normalizer are created only if needed
if ($this->entityOneNormalizer == null)
$this->entityOneNormalizer = new EntityOneNormalizer($this); //every normalizer keep a reference to this service
return $this->entityOneNormalizer;
}
//create a function for each normalizer
//the serializer service will also serialize the entities
//(i found it easier, but you don't really need it)
public function serialize($objects, $format)
{
$serializer = new Serializer(
array(
$this->getEntityOneNormalizer(),
$this->getEntityTwoNormalizer()
),
array($format => $encoder) );
return $serializer->serialize($response, $format);
}
An example of a Normalizer :
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PlaceNormalizer implements NormalizerInterface {
private $normalizerService;
public function __construct($normalizerService)
{
$this->service = normalizerService;
}
public function normalize($object, $format = null) {
$entityTwo = $object->getEntityTwo();
$entityTwoNormalizer = $this->service->getEntityTwoNormalizer();
return array(
'param' => object->getParam(),
//repeat for every parameter
//!!!! this is where the entityOneNormalizer dealt with recursivity
'entityTwo' => $entityTwoNormalizer->normalize($entityTwo, $format.'_without_any_entity_one') //the 'format' parameter is adapted for ignoring entity one - this may be done with different ways (a specific method, etc.)
);
}
}
In a controller :
$normalizerService = $this->get('normalizer.service'); //you will have to configure services.yml
$json = $normalizerService->serialize($myobject, 'json');
return new Response($json);
The complete code is here : https://github.com/progracqteur/WikiPedale/tree/master/src/Progracqteur/WikipedaleBundle/Resources/Normalizer
in Symfony 2.3
/app/config/config.yml
framework:
# сервис конвертирования объектов в массивы, json, xml и обратно
serializer:
enabled: true
services:
object_normalizer:
class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
tags:
# помечаем к чему относится этот сервис, это оч. важно, т.к. иначе работать не будет
- { name: serializer.normalizer }
and example for your controller:
/**
* Поиск сущности по ИД объекта и ИД языка
* #Route("/search/", name="orgunitSearch")
*/
public function orgunitSearchAction()
{
$array = $this->get('request')->query->all();
$entity = $this->getDoctrine()
->getRepository('IntranetOrgunitBundle:Orgunit')
->findOneBy($array);
$serializer = $this->get('serializer');
//$json = $serializer->serialize($entity, 'json');
$array = $serializer->normalize($entity);
return new JsonResponse( $array );
}
but the problems with the field type \DateTime will remain.
This is more an update (for Symfony v:2.7+ and JmsSerializer v:0.13.*#dev), so to avoid that Jms tries to load and serialise the whole object graph ( or in case of cyclic relation ..)
Model:
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Exclude;
use JMS\Serializer\Annotation\MaxDepth; /* <=== Required */
/**
* User
*
* #ORM\Table(name="user_table")
///////////////// OTHER Doctrine proprieties //////////////
*/
public class User
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="FooBundle\Entity\Game")
* #ORM\JoinColumn(nullable=false)
* #MaxDepth(1)
*/
protected $game;
/*
Other proprieties ....and Getters ans setters
......................
......................
*/
Inside an Action:
use JMS\Serializer\SerializationContext;
/* Necessary include to enbale max depth */
$users = $this
->getDoctrine()
->getManager()
->getRepository("FooBundle:User")
->findAll();
$serializer = $this->container->get('jms_serializer');
$jsonContent = $serializer
->serialize(
$users,
'json',
SerializationContext::create()
->enableMaxDepthChecks()
);
return new Response($jsonContent);
If you are using Symfony 2.7 or above, and don't want to include any additional bundle for serializing, maybe you can follow this way to seialize doctrine entities to json -
In my (common, parent) controller, I have a function that prepares the serializer
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
// -----------------------------
/**
* #return Serializer
*/
protected function _getSerializer()
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$normalizer = new ObjectNormalizer($classMetadataFactory);
return new Serializer([$normalizer], [new JsonEncoder()]);
}
Then use it to serialize Entities to JSON
$this->_getSerializer()->normalize($anEntity, 'json');
$this->_getSerializer()->normalize($arrayOfEntities, 'json');
Done!
But you may need some fine tuning. For example -
If your entities have circular reference, check how to handle it.
If you want to ignore some properties, can do it
Even better, you can serialize only selective attributes.
When you need to create a lot of REST API endpoints on Symfony,
the best way is to use the following stack of bundles:
JMSSerializerBundle for the serialization of Doctrine entities
FOSRestBundle bundle for response view listener. Also, it can generate definitions of routes based on controller/action name.
NelmioApiDocBundle to auto-generate online documentation and Sandbox(which allows testing endpoint without any external tool).
When you configure everything properly, you entity code will look like this:
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;
/**
* #ORM\Table(name="company")
*/
class Company
{
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255)
*
* #JMS\Expose()
* #JMS\SerializedName("name")
* #JMS\Groups({"company_overview"})
*/
private $name;
/**
* #var Campaign[]
*
* #ORM\OneToMany(targetEntity="Campaign", mappedBy="company")
*
* #JMS\Expose()
* #JMS\SerializedName("campaigns")
* #JMS\Groups({"campaign_overview"})
*/
private $campaigns;
}
Then, code in controller:
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use FOS\RestBundle\Controller\Annotations\View;
class CompanyController extends Controller
{
/**
* Retrieve all companies
*
* #View(serializerGroups={"company_overview"})
* #ApiDoc()
*
* #return Company[]
*/
public function cgetAction()
{
return $this->getDoctrine()->getRepository(Company::class)->findAll();
}
}
The benefits of such a set up are:
#JMS\Expose() annotations in the entity can be added to simple fields, and to any type of relations. Also, there is the possibility to expose the result of some method execution (use annotation #JMS\VirtualProperty() for that)
With serialization groups, we can control exposed fields in different situations.
Controllers are very simple. The action method can directly return an entity or array of entities, and they will be automatically serialized.
And #ApiDoc() allows testing the endpoint directly from the browser, without any REST client or JavaScript code
Now you can also use Doctrine ORM Transformations to convert entities to nested arrays of scalars and back
The accepted answer is correct but if You'll need to serialize a filtered subset of an Entity , json_encode is enough:
Consider this example:
class FileTypeRepository extends ServiceEntityRepository
{
const ALIAS = 'ft';
const SHORT_LIST = 'ft.name name';
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, FileType::class);
}
public function getAllJsonFileTypes()
{
return json_encode($this->getAllFileTypes());
}
/**
* #return array
*/
public function getAllFileTypes()
{
$query = $this->createQueryBuilder(self::ALIAS);
$query->select(self::SHORT_LIST);
return $query->getQuery()->getResult();
}
}
/** THIS IS ENOUGH TO SERIALIZE AN ARRAY OF ENTITIES SINCE the doctrine SELECT will remove complex data structures from the entities itself **/
json_encode($this->getAllFileTypes());
Short note: Tested at least on Symfony 5.1