Magento work with cache, can't serialize collection - magento

I'm learning how to use magento cache and I'm a bit stuck trying to serialize a collection.
Actually this is my code:
class Feliu_Featuredcategories_Block_Topcategories extends Mage_Core_Block_Template
{
protected function _construct()
{
$storeId = Mage::app()->getStore()->getId();
$this->addData(array(
'cache_lifetime' => 3600,
'cache_tags' => array(Mage_Catalog_Model_Product::CACHE_TAG),
'cache_key' => 'homepage-most-view-' . $storeId,
));
}
public function setData()
{
$storeId = Mage::app()->getStore()->getId();
$cache = Mage::app()->getCache();
$key = 'homepage-most-view-' . $storeId;
$cached_categories = $cache->load($key);
if (! $cached_categories) {
$categories = Mage::getModel('catalog/category')
->getCollection()
->addAttributeToSelect(array('data', 'name', 'add_to_top_categories'))
->addAttributeToFilter('add_to_top_categories', array('eq' => '1'));
$categories->load();
$cache->save(serialize($categories), $key);
} else {
$categories = unserialize($cached_categories);
}
return $categories;
}
}
At first I tried to $cache->save($categories, $key); directly, but I read that collections can't be saved directly and I got an error that said: 'automatic_serialization must be on' when I tried to set automatic_serialization to true then I received a message saying that it can't be activated for security reasons.
Then I tried to serialize, just as the above code shows, but it did not work neither. It seems that magento protects collections from being serialized because they can be really big.
So finally I tried to urlencode() before serializing serialize(urlencode($categories)) and urldecode(unserialize($categories)) but I got the string "N;" serializing with this aproach and an empty string when unserialize.
I'm using magento 1.9.3 and I followed this documentation and previous questions:
https://www.nicksays.co.uk/developers-guide-magento-cache/
http://inchoo.net/magento/magento-block-caching/
Magento: serialization error on caching Collection
Magento how to cache a productCollection
And some other questions about this, but maybe there is no need to write too much links, I don't want to spam.
Edit: If instead a collection I use an array like
$categories = array('banana', 'apple', 'kiwi', 'strawberry', 'pomelo', 'melon');
then the code seems to work correctly

Finally I solved it, the answer it's easiest than I though at the beginning but I write it here because maybe it will help somebody in the future.
As collections cannot be cached nor serialized, I made an array with the data I need from the collection.
$categories = Mage::getModel('catalog/category')
->getCollection()
->addAttributeToFilter('add_to_top_categories', array('eq' => '1'))
->addAttributeToSelect(array('data', 'name'));
I make the collection adding only the fields I need, and selecting the data I want.
$array = array();
foreach ($categories as $_category)
{
array_push($array, array('url' => $_category->getUrl(), 'name' => $_category->getName()));
}
Now I make an array that holds objects with the data I wanted. Next step is serialize the array I just made and save it on the cache.
$cache->save(serialize($array), $key, array('custom_home_cache'), 60*60);
and retrieve the data is as easy as $cache->load(unserialize($key))

Related

how to check if cart has already added not allow more and update if different cart

I'm using composer require gloudemans/shoppingcart I am not sure how to maintain amount.
When i'm using one route that says add item when i'm using this route adding multiple item
How can i conditionally setup to add item in cart if this is unique
public function bookItem($id) {
$item = Item::where([
'status' => '1',
'id' => $id
])->first();
$product = Cart::add($item->id, $item->name, 1, $item->price); // This should not call always if it has not generated a row id then it should call
Cart::update($product->rowId, ['price' => 200]); // Will update the price if it is differ
return redirect()->route('book.item', ['id' => $item->id]);
}
I am not sure how to manage it. please guide
Looks like the package has a getContents() function that gathers all items into a collection. It also has a search(Closure) function that calls getContents() and then uses Laravel's filter function on the collection and returns the result.
$search = Cart::search(function($key,$value) use ($item) {
return $value === $item->id;
})->first();
if(!empty($search)){
Cart::update($search->rowId, ['price' => 200]);
}
else {
$product = Cart::add($item->id, $item->name, 1, $item->price);
}
Definitely check out the Laravel collection docs if you aren't familiar. This is the entey on filters:
https://laravel.com/docs/5.8/collections#method-filter

Fetching a cached Eloquent collection of 10 items times out

I'm building a search functionality that returns large collections which are paginated using a LengthAwarePaginator. I'm trying to cache results using a key called $searchFilter_$query_$offsetPages for a single page of returned results (10 items). It goes into the cache just fine. However, it times out when I try to check using Cache::has($key) or fetch using Cache::get($key).
The same problem occurs in the browser as well as in artisan Tinker. Strangely, when I put a random set of 10 items into the cache in Tinker and fetch them back, everything works fine. I'm using Redis as the cache driver.
Here is my controller method:
public function search($filter, $query, $layout, Request $request) {
if($layout == "list-map") {
return view("list-map")->with(['filter' => $filter, 'query' => $query, 'layout' => 'list-map']);
} else {
$offsetPages = $request->input('page', 1) - 1;
$cacheKey = $filter . "_" . $query . "_" . $offsetPages;
if(Cache::has($cacheKey)) {
\Log::info("fetching results from cache");
$data = Cache::get($cacheKey);
$totalCt = $data[0];
$results = $data[1];
} else {
$results = $this->getResults($filter, $query);
$totalCt = $results->count();
$results = $results->slice($offsetPages, $this->resultsPerPage);
\Log::info("caching results");
Cache::put($cacheKey, [$totalCt, $results], 5);
}
$results = new LengthAwarePaginator($results,
$totalCt,
$this->resultsPerPage,
$request->input('page', 1),
['path' => LengthAwarePaginator::resolveCurrentPath()]
);
return view($layout)->with(['filter' => $filter, 'query' => $query, 'layout' => $layout, 'results' => $results]);
}
}
So, the issue was that many of the models in the collection returned from my getResults() method were obtained via relationship queries. When I would dd($results) on the single page of 10 results, I could see that there was a "relations" field on each model. Inside that array were thousands of recursively related models based on the relationship I originally queried. I was unable to find any information about an option to not eager load these related models. Instead I came up with a bit of a hacky workaround to fetch the models directly:
$results = $results->slice($offsetPages, $this->resultsPerPage);
//load models directly so they don't include related models.
$temp = new \Illuminate\Database\Eloquent\Collection;
foreach($results as $result) {
if(get_class($result) == "App\Doctor") {
$result = Doctor::find($result->id);
} else if(get_class($result == "App\Organization")) {
$result = Organization::find($result->id);
}
$temp->push($result);
}
$results = $temp;
\Log::info("caching results");
Cache::put($cacheKey, [$totalCt, $results], 5);
If anyone knows the best practice in this situation, please let me know. Thanks!
Edit:
I've found a better solution instead of the above workaround. If I query my relationships like this: $taxonomy->doctors()->get() rather than $taxonomy->doctors, it does not load in the huge recusive relations.
I dont really see why your code doesn't work. The only potential problems I see are the cache keys, which could contain problematic characters, as well as the way you check for a cached value. As you are using Cache::has($key) before Cache::get($key), you could end up with a race condition where the first call returns true and the latter null because the cached value timed out just between the two calls.
I tried to address both issues in the following snippet:
public function search($filter, $query, $layout, Request $request)
{
if($layout == "list-map") {
return view("list-map")->with(['filter' => $filter, 'query' => $query, 'layout' => 'list-map']);
} else {
$offsetPages = $request->input('page', 1) - 1;
$cacheKey = md5("{$filter}_{$query}_{$offsetPages}");
$duration = 5; // todo: make this configurable or a constant
[$totalCount, $results] = Cache::remember($cacheKey, $duration, function () use ($filter, $query) {
$results = $this->getResults($filter, $query);
$totalCount = $results->count();
$filteredResults = $results->slice($offsetPages, $this->resultsPerPage);
return [$totalCount, $filteredResults];
});
$results = new LengthAwarePaginator($results,
$totalCount,
$this->resultsPerPage,
$request->input('page', 1),
['path' => LengthAwarePaginator::resolveCurrentPath()]
);
return view($layout)->with(compact('filter', 'query', 'layout', 'results'));
}
}
The inbuilt function Cache::remember() doesn't use Cache::has() under the hood. Instead, it will simply call Cache::get(). As this function will return null as default if no cache was hit, the function can easily determine if it has to execute the closure or not.
I also wrapped the $cacheKey in md5(), which gives a consistently valid key.
Looking at the following part of your code
$results = $this->getResults($filter, $query);
$totalCount = $results->count();
$filteredResults = $results->slice($offsetPages, $this->resultsPerPage);
I am quite sure the whole search could be improved (independently of the caching). Because it seems you are loading all results for a specific search into memory, even if you throw away most parts of it. There is certainly a better way to do this.

How to unset (remove) a collection element after fetching it?

I have a collection which I want to iterate and modify while I fetch some of its elements. But I could't find a way or method to remove that fetched element.
$selected = [];
foreach ($collection as $key => $value) {
if ($collection->selected == true) {
$selected[] = $value;
unset($value);
}
}
This is just a representation of my question for demonstration.
After #Ohgodwhy advice the forget() function I checked it again and saw that I actually misunderstood the function. It was exactly as I was looking for.
So for working solution I have added $collection->forget($key) inside the if statement.
Below is the working solution of my problem, using #Ohgodwhy's solution:
$selected = [];
foreach ($collection as $key => $value) {
if ($collection->selected == true) {
$selected[] = $value;
$collection->forget($key);
}
}
(this is just a demonstration)
You would want to use ->forget()
$collection->forget($key);
Link to the forget method documentation
Or you can use reject method
$newColection = $collection->reject(function($element) {
return $item->selected != true;
});
or pull method
$selected = [];
foreach ($collection as $key => $item) {
if ($item->selected == true) {
$selected[] = $collection->pull($key);
}
}
Laravel Collection implements the PHP ArrayAccess interface (which is why using foreach is possible in the first place).
If you have the key already you can just use PHP unset.
I prefer this, because it clearly modifies the collection in place, and is easy to remember.
foreach ($collection as $key => $value) {
unset($collection[$key]);
}
I'm not fine with solutions that iterates over a collection and inside the loop manipulating the content of even that collection. This can result in unexpected behaviour.
See also here: https://stackoverflow.com/a/2304578/655224 and in a comment the given link http://php.net/manual/en/control-structures.foreach.php#88578
So, when using foreach it seems to be ok but IMHO the much more readable and simple solution is to filter your collection to a new one.
/**
* Filter all `selected` items
*
* #link https://laravel.com/docs/7.x/collections#method-filter
*/
$selected = $collection->filter(function($value, $key) {
return $value->selected;
})->toArray();
If you know the key which you unset then put directly by comma
separated
unset($attr['placeholder'], $attr['autocomplete']);
You can use methods of Collection like pull, forget, reject etc.
But every Collection method returns an entirely new Collection instance.
Doc link: https://laravel.com/docs/8.x/collections#introduction

Laravel convert an array to a model

i have an array as follows
'topic' =>
array (
'id' => 13,
'title' => 'Macros',
'content' => '<p>Macros. This is the updated content.</p>
',
'created_at' => '2014-02-28 18:36:55',
'updated_at' => '2014-05-14 16:42:14',
'category_id' => '5',
'tags' => 'tags',
'referUrl' => '',
'user_id' => 3,
'videoUrl' => '',
'useDefaultVideoOverlay' => 'true',
'positive' => 0,
'negative' => 1,
'context' => 'macros',
'viewcount' => 60,
'deleted_at' => NULL,
)
I would like to use this array and convert/cast it into the Topic Model . Is there a way this can be done.
thanks
Try creating a new object and passing the array into the constructor
$topic = new Topic($array['topic']);
For creating models from a single item array:
$Topic = new Topic();
$Topic->fill($array);
For creating a collection from an array of items:
$Topic::hydrate($result);
Here is a generic way to do it, not sure if there is a Laravel-specific method -- but this is pretty simple to implement.
You have your Topic class with its properties, and a constructor that will create a new Topic object and assign values to its properties based on an array of $data passed as a parameter.
class Topic
{
public $id;
public $title;
public function __construct(array $data = array())
{
foreach($data as $key => $value) {
$this->$key = $value;
}
}
}
Use it like this:
$Topic = new Topic(array(
'id' => 13,
'title' => 'Marcos',
));
Output:
object(Topic)#1 (2) {
["id"]=>
int(13)
["title"]=>
string(6) "Marcos"
}
It seems that you have data of an existing model there, so:
First, you can use that array to fill only fillable (or not guarded) properties on your model. Mind that if there is no fillable or guarded array on the Topic model you'll get MassAssignmentException.
Then manually assign the rest of the properties if needed.
Finally use newInstance with 2nd param set to true to let Eloquent know it's existing model, not instantiate a new object as it would, again, throw an exception upon saving (due to unique indexes constraints, primary key for a start).
.
$topic = with(new Topic)->newInstance($yourArray, true);
$topic->someProperty = $array['someProperty']; // do that for each attribute that is not fillable (or guarded)
...
$topic->save();
To sum up, it's cumbersome and probably you shouldn't be doing that at all, so the question is: Why you'd like to do that anyway?
Look at these two available methods in L5 newInstance and newFromBuilder
e.g with(new static)->newInstance( $attributes , true ) ;
I would likely create the new instance of the object and then build it that way, then you can actually split some useful reusable things or defaults into the model otherwise what's the point in pushing an array into a model and doing nothing with it - very little besides for normalization.
What I mean is:
$topic = new Topic();
$topic->id = 3489;
$topic->name = 'Test';
And the model would simply be a class with public $id;. You can also set defaults so if you had like resync_topic or whatever property, you can set it as 0 in the model rather than setting 0 in your array.
I came across this question looking for something else. Noticed it was a bit outdated and I have another way that I go about handling the OPs issue. This might be a known way of handling the creation of a model from an array with more recent versions of Laravel.
I add a generic constructor to my class/model
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
}
Then when I want to create a new instance of the model from an array I make a call like this
$topic = new Topic($attrs);
// Then save it if applicable
$topic->save(); // or $topic->saveOrFail();
I hope someone finds this helpful.

Get list of all product attributes in magento

I have been doing frontend magento for a while but have only just started building modules. This is something i know how to do frontend but i am struggling with in my module. What i am trying to achieve for now, is populating a multiselect in the admin with all available product attributes. Including custom product attributes across all product attribute sets. I'm not entirely sure what table this will require because i don't want to assume that Flat Category Data is enabled.
I have created my admin area in a new tab in system config, i have created a multiselect field that is currently just being populated with three static options. This much works. Could anyone help me by pointing a finger in the right direction... currently this is what i have so far (for what it's worth).
<?php
class test_test_Model_Source
{
public function toOptionArray()
{
return array(
array('value' => 0, 'label' =>'First item'),
array('value' => 1, 'label' => 'Second item'),
array('value' => 2, 'label' =>'third item'),
);
}
}
///////////////////////////// EDIT /////////////////////////////////////
I feel like i might be onto something here, but it's only returning the first letter of every attribute (so i'm not sure if its even the attributes its returning)
public function toOptionArray()
{
$attributes = Mage::getModel('catalog/product')->getAttributes();
$attributeArray = array();
foreach($attributes as $a){
foreach($a->getSource()->getAllOptions(false) as $option){
$attributeArray[$option['value']] = $option['label'];
}
}
return $attributeArray;
}
///////////////////////////////// EDIT //////////////////////////////////////
I am not extremely close as i now know that the array is returning what i want it to, all attribute_codes. However it is still only outputting the first letter of each... Anyone know why?
public function toOptionArray()
{
$attributes = Mage::getModel('catalog/product')->getAttributes();
$attributeArray = array();
foreach($attributes as $a){
foreach ($a->getEntityType()->getAttributeCodes() as $attributeName) {
$attributeArray[$attributeName] = $attributeName;
}
break;
}
return $attributeArray;
}
I have answered my own question. I have found a way that worked however i'm not sure why, so if someone could comment and explain that would be useful. So although having $attributeArray[$attributeName] = $attributeName; worked when it came to a print_r when you returned the array it was only providing the first letter. However if you do the following, which in my opinion seems to be doing exactly the same thing it works. I can only imagine that when rendering it wasn't expecting a string but something else. Anyway, here is the code:
public function toOptionArray()
{
$attributes = Mage::getModel('catalog/product')->getAttributes();
$attributeArray = array();
foreach($attributes as $a){
foreach ($a->getEntityType()->getAttributeCodes() as $attributeName) {
//$attributeArray[$attributeName] = $attributeName;
$attributeArray[] = array(
'label' => $attributeName,
'value' => $attributeName
);
}
break;
}
return $attributeArray;
}
No need to do additional loops, as Frank Clark suggested. Just use:
public function toOptionArray()
{
$attributes = Mage::getResourceModel('catalog/product_attribute_collection')->addVisibleFilter();
$attributeArray = array();
foreach($attributes as $attribute){
$attributeArray[] = array(
'label' => $attribute->getData('frontend_label'),
'value' => $attribute->getData('attribute_code')
);
}
return $attributeArray;
}
You can try to get attributes in other way, like this
$attributes = Mage::getSingleton('eav/config')
->getEntityType(Mage_Catalog_Model_Product::ENTITY)->getAttributeCollection();
Once you have attributes you can get options in this way (copied from magento code)
$result = array();
foreach($attributes as $attribute){
foreach ($attribute->getProductAttribute()->getSource()->getAllOptions() as $option) {
if($option['value']!='') {
$result[$option['value']] = $option['label'];
}
}
}

Resources