I use the symfony serializer to deserialize REST server answers to objects.
The data returned by the server is someting like this (pseudocode, the answer itself is JSON):
// Endpoint 1
class Paginated {
public items:Object1[]
public page:int
}
// Endpoint 2
class Paginated {
public: items:Object2[]
public page:int
}
So, every answer is wrapped in the same "Paginated" object.
Sice I don't want to repeat the common members in every object I want to implement the objects in my Symfony app the same way as described in this pseudo code.
The problem is, that PHP isn't supporting generics to typehint the "items" member and the symfony serializer doesn't seem to offer something similar.
So, whats the best way to tackle this problem?
An easy way to solve the issue is by using the callbacks context option. In that callback you can just deserialize the objects you are going to pass into Paginated yourself. You would then have different callbacks for each object-type you want to support in Paginated and register it. It doesn't have to be a closure like shown in the docs, you can also use a class with __invoke() to make it easier to reuse in different places.
Another way to solve this more generically is, by writing a custom Denormalizer that implements the DenormalizerAwareInterface (so it can delegate denormalization of the nested items back to the serializer.
Much like Symfony can recognize something like Object[] as a list of Objects, you can created your own custom type convention to simulate a Generic.
Assuming you want this to be something like Paginated<Object1>, then your serializer call would probably look something like this:
$serializer->deserialize($json, Paginated::class . '<' . Object1::class . '>', 'json');
Your (de)normalizer will then support the type matching the regex. Inside the denormalize method you would then take the array structure of your json, call something like `$denormalizedItems = $this->denormalizer->denormalize($data['items'], Object1::class . '[]'); and then put them into your Paginated object. Roughly like this:
public function denormalize($data, string $type, string $format = null, array $context = [])
{
$extractedObjectType = ...; #extract class name inside <>
$data['items'] = $this->denormalizer->denormalize($data['items'], $extractedType, $format, $context);
// Option 1: Delegate denormalizing Paginated with the adjusted data
return $this->denormalizer->denormalize($data, Paginated::class, $format, $context);
// Option 2: Denormalize Paginated yourself and pass adjusted data as argument
return new Paginated($data['items'], (int) $data['page']);
}
Related
According to FOSElasticaBundle documentation it is possible to configure application to use custom query builder method like this:
user:
persistence:
elastica_to_model_transformer:
query_builder_method: createSearchQueryBuilder
But is it possible to choose QB method live, e.g. in controller action?
I'd like to be able to control what's being fetched from DB while transforming Elastica results to Doctrine entities. E.g. sometimes I'll want to do eager fetch on some relations, but can't do that by default.
Since FOSElasticaBundle documentation is not very precise, I went through its code and found it impossible to control what query builder is used on controller level.
It is possible to change whole elastica_to_model_transformer to a custom service, but still it's statically defined in configuration. Maybe with some dirty solution it would be possible going this way, but I don't think it's worth it.
I decided to just not using this feature of FOSElasticaBundle. The main problem I had was that when you use fos_elastica.index instead of fos_elastica.finder or elastica repository (in order to get plain not transformed results Elastica\Resultset), there's no findPaginated method with returns Pagerfanta paginator object, which is very helpful in my case.
Fortunately although it's not mentioned in documentation it's possible to create the Pagerfanta this way too, but a little bit more manually.
Here's a code snippet:
//generate ElaticaQuery somehow.
$browseQuery = $browseData->getBrowseQuery();
$search = $this->container->get('fos_elastica.index.indexName.typName');
//create pagerfanta's adapter manually
$adapter = new \Pagerfanta\Adapter\ElasticaAdapterElasticaAdapter($search, $browseQuery);
// now you can create the paginator too.
$pager = new Pagerfanta($adapter);
//do some paging work on it...
$pager->setMaxPerPage($browseData->getPerPage());
try {
$pager->setCurrentPage($browseData->getPage());
} catch(OutOfRangeCurrentPageException $e) {
$pager->setCurrentPage(1);
}
//and get current page results.
/** #var Result[] $elasticaResults */
$elasticaResults = $pager->getCurrentPageResults();
// we have to grab ids manyally, but it's done the same way inside FOSElasticaBundle with previous approach
$ids = array();
foreach($elasticaResults as $elasticaResult) {
$ids[] = $elasticaResult->getId();
}
//use regular Doctrine's repository to fetch Entities any way you want.
$entities = $this->getDoctrine()->getRepository(MyEntity::class)->findByIdentifiers($ids);
This actually has a few advantages. In general it gives you back control over your data and doesn't tie ElasticSearch with Doctrine. Therefore you can resign on fetching data from Doctrine if you have all needed data in ElasticSearch (if they are read only data of course). This lets you optimize your application performance but reducing amount of SQL queries.
The code above may be wrapped with some kind of service in order to prevent making mess in controllers.
I am using the Fractal library to transform a Book object into JSON by using a simple transformer:
class BookTransformer extends \League\Fractal\TransformerAbstract
{
public function transform(Book $book)
{
return [
'name' => $book->getName()
// ...
];
}
}
And I am performing the transformation as follows.
$book = new Book('My Awesome Book');
$resource = new \League\Fractal\Resource\Item($book, new BookTransformer());
$fractal = new \League\Fractal\Manager();
$fractal->setSerializer(new \League\Fractal\Serializer\ArraySerializer());
$json = $fractal->createData($resource)->toJson();
This works great. However, I have certain fields on my Book object that should not always be included, because this depends on the context the transformation is done in. In my particular use case, the JSON returned to AJAX requests from my public website should not include sensitive information, while this should be the case when the data is requested from an admin backend.
So, let's say that a book has a topSecretValue field, which is a string. This field should not be included in one transformation, but should be included in another. I took a look at transformer includes, and played around with it, but this only works with resources. In my case, I need to somehow include different fields (not resources) for different contexts. I have been digging around and could not find anything in the Fractal library that could help me, but maybe I am missing something?
I came up with a working solution, but it is not the prettiest the world has ever seen. By having a BaseBookTransformer that transforms fields that should always be included, I can extend this transformer to add fields for other contexts, e.g. AdminBookTransformer or TopSecretValueBookTransformer, something like the below.
class AdminBookTransformer extends BookTransformer
{
public function transform(Book $book)
{
$arr = parent::transform($book);
$arr['author'] = $book->getTopSecretValue();
return $arr;
}
}
This works fine, although it is not as "clean" as using includes (if it were possible), because I have to actually use a different transformer.
So the question is: is there anything in Fractal that enables me to accomplish this in a simpler/cleaner way, or is there a better way to do it, be it the Fractal way or not?
This is probably very easy to do, but I can't seem to get my head around it right now. Let's say in a component in a cakephp application, I have a variable my_model, which contains the model of the corresponding controller that is currently using the component like:
function TestComponent extend Object
{
var $my_model; // can be either User, or Person
function test()
{
$myModelTemp = $this->my_model;
$model = $myModelTemp != 'User' ? $myModelTemp.'->User' : 'User';
$this->$model->find('all');
}
}
As you can see above in my function test() what I'm trying to do is call the correct model based on the value of my_model. So based on the condition, my query will be either:
$this->Person->User->find('all');
Or
$this->User->find('all');
When I do it like I did above, I get an error saying Fatal error: Call to a member function find() on a non-object. In order words, that error means Person->User is not an object (so, it is considered as a string).
What you're saying could be true, however, it can refer to any part of the call.
So either Person or User could be invalid, or together they causes the error. Hard to say.
Try dumping the individual objects using var_dump();
So try:
<?php
echo "<pre>";
var_dump(is_object($this->Person));
var_dump(is_object($this->User));
echo "</pre>";
?>
to determine where you're code goes wrong.
To be clear, that return value needs to be true for it to be an object.
The one that returns false is the likely culprit.
Should your question refer to the correct way to reference an object, an object is basically an array. For example:
<?php
$obj = (object) array("this", "my_function");
?>
The above example casts the array as an object. However, using multiple layers might prove to be more difficult than you'd expect.
Generally, it looks like you might be going about this all wrong. Obviously you want the models to be dynamic, but then you're hard-coding things which defeats the whole point of it being dynamic in the first place.
It also seems like you might be violating the principals of CakePHP and MVC by doing all this in a component. I'm not sure this component should really be manipulating models or assuming which models are currently in use.
However, if you want to evaluate a string as an actual object, you can wrap it in { ... } (this is valid standard PHP syntax, not Cake-specific code).
Try this:
$modelName = $this->my_model;
$model = ($modelName != 'User') ? $this->{$modelName}->User : $this->User;
$model->find('all');
Now, if this doesn't work or you get an error saying it can't find the model(s) you need to ensure the models are actually loaded and initialised in the current scope.
I'm a little confused as to what is going on here, it looks to me like a method is calling itself? I'm trying to learn about Magento's models. I was working my way back from a helper (catalog/category) and I got to a call on this method "GetCategories". I don't know whats going on here. If anyone could shed light on this code snippet I greatly appreciate it.
getCategories ( $parent,
$recursionLevel = 0,
$sorted = false,
$asCollection = false,
$toLoad = true
){
$categories = $this->getResource()
->getCategories($parent, $recursionLevel, $sorted, $asCollection, $toLoad);
return $categories;
}
Not much to add to #hakra's answer. Just a portion of Magento-specific logic.
So to work with Magento models you should know, that Magento has 2 types of Models: normal models, and resource models (we can call assign Blocks to the models too, as a view models - but that is more connected to the V part of MVC).
The resource models were created as a DB adapters that contain only DB-related logic, and often are connected to some DB table, hence contain the logic for CRUD operations with that table. So you'll see smth like this regularly - for the simplicity someMethod is a part of normal model, but since it contains DB-related logic, all the implementation of the method was moved to the resource model, so the body of someMethod in the regular model will be something like that:
public function someMethod($args)
{
return $this->getResource()->someMethod($args);
}
It is hard to say for the code you've posted. Even both methods share the same name (getCategories) it must not mean that they are of the same class or even object.
If you want to find out you would need to compare:
var_dump($this === $this->getResource());
Apart from that, it is also common in programming recursion that a method calls itself, hence recursion. However for that chunk of code, it would run against the wall.
So technically speaking I would do the assumption that in your example this is not the exact same object method.
Please take note that this answer is independent to Magento, it's just how PHP works generally.
I am currently a beginner in CakePHP, and have played around with CakePHP 1.3, but recently CakePHP 2.0 has been released.
So far I like it but the only thing is being a pain is the fact that it doesn't return Objects, rather it just returns arrays. I mean, it hardly makes sense to have to do $post['Post']['id']. It is (in my opinion) much more practical to just do $post->id.
Now after Google I stumbled upon this link, however, this kept generating errors about indexes not being defined when using the Form class (guessing this is because it was getting the objectified version rather than the array version).
I am following the Blog tutorial (already have followed it under 1.3 but going over it again for 2.0)
So, anyone know how to achieve this without it interfering with the Form class?
Hosh
Little known fact: Cake DOES return them as objects, or well properties of an object, anyway. The arrays are the syntactical sugar:
// In your View:
debug($this->viewVars);
Shwoing $this is a View object and the viewVars property corresponds with the $this->set('key', $variable) or $this->set(compact('data', 'for', 'view')) from the controller action.
The problem with squashing them into $Post->id for the sake of keystrokes is Cake is why. Cake is designed to be a heavy lifter, so its built-in ORM is ridiculously powerful, unavoidable, and intended for addressing infinity rows of infinity associated tables - auto callbacks, automatic data passing, query generation, etc. Base depth of multidimensional arrays depends on your find method, as soon as you're working with more than one $Post with multiple associated models (for example), you've introduced arrays into the mix and there's just no avoiding that.
Different find methods return arrays of different depths. From the default generated controller code, you can see that index uses $this->set('posts', $this->paginate()); - view uses $this->set('post', $this->Post->read(null, $id)); and edit doesn't use $this->set with a Post find at all - it assigns $this->data = $this->Post->read(null, $id);.
FWIW, Set::map probably throws those undefined index errors because (guessing) you happen to be trying to map an edit action, amirite? By default, edit actions only use $this->set to set associated model finds to the View. The result of $this->read is sent to $this->data instead. That's probably why Set::map is failing. Either way, you're still going to end up aiming at $Post[0]->id or $Post->id (depending on what you find method you used), which isn't much of an improvement.
Here's some generic examples of Set::map() property depth for these actions:
// In posts/index.ctp
$Post = Set::map($posts);
debug($Post);
debug($Post[0]->id);
// In posts/edit/1
debug($this-viewVars);
debug($this->data);
// In posts/view/1
debug($this-viewVars);
$Post = Set::map($post);
debug($Post->id);
http://api13.cakephp.org/class/controller#method-Controllerset
http://api13.cakephp.org/class/model#method-Modelread
http://api13.cakephp.org/class/model#method-ModelsaveAll
HTH.
You could create additional object vars. This way you wouldn't interfere with Cake's automagic but could access data using a format like $modelNameObj->id; format.
Firstly, create an AppController.php in /app/Controller if you don't already have one. Then create a beforeRender() function. This will look for data in Cake's standard naming conventions, and from it create additional object vars.
<?php
App::uses('Controller', 'Controller');
class AppController extends Controller {
public function beforeRender() {
parent::beforeRender();
// camelcase plural of current model
$plural = lcfirst(Inflector::pluralize($this->modelClass));
// create a new object
if (!empty($this->viewVars[$plural])) {
$objects = Set::map($this->viewVars[$plural]);
$this->set($plural . 'Obj', $objects);
}
// camelcase singular of current model
$singular = lcfirst(Inflector::singularize($this->modelClass));
// create new object
if (!empty($this->viewVars[$singular])) {
$object = Set::map($this->viewVars[$singular]);
$this->set($singular . 'Obj', $object);
}
}
}
Then in your views you can access the objects like so:
index.ctp
$productsObj;
view.ctp
$productObj->id;
All we're doing is adding 'Obj' to the variable names that Cake would already provide. Some example mappings:
Products -> $productsObj
ProductType -> $productTypesObj
I know this is not perfect but it would essentially achieve what you wanted and would be available across all of your models.
While I like the idea Moz proposes there are a number of existing solutions to this problem.
The quickest one I found is https://github.com/kanshin/CakeEntity - but it looks like you might need to refactor it for 2.x - there might even already be a 2.x branch or fork but I didn't look.
I also ran this question couple of time in my head. Now a few Cake based apps later, I see the benefit to be able to branch and merge (am, in_array etc.) result sets more conveniently with arrays than using objects.
The $Post->id form would be a sweet syntactic sugar, but not a real benefit over arrays.
You could write a function that iterates over your public propertys (see ReflectionClass::getProperties) and save it in an array (and return the array).
If you have access to the class, you can implement the ArrayAccess Interface and easily access your object as an array.
P.S.: Sorry, i've never used CakePHP but i think object-to-array conversion doesn't have to be a framework specific problem