Hi well the problem I am facing seemed to be very simple at first but turned into a real nightmare now.
I was asked to add an attribute (namely point) to all the products (which was done pretty simple using the admin panel) and have its total as a cart attribute which rules can be set upon!?
I am quite positive that cart attributes are defined in:
class Mage_SalesRule_Model_Rule_Condition_Address extends Mage_Rule_Model_Condition_Abstract
{
public function loadAttributeOptions()
{
$attributes = array(
'base_subtotal' => Mage::helper('salesrule')->__('Subtotal'),
'total_qty' => Mage::helper('salesrule')->__('Total Items Quantity'),
'weight' => Mage::helper('salesrule')->__('Total Weight'),
'payment_method' => Mage::helper('salesrule')->__('Payment Method'),
'shipping_method' => Mage::helper('salesrule')->__('Shipping Method'),
'postcode' => Mage::helper('salesrule')->__('Shipping Postcode'),
'region' => Mage::helper('salesrule')->__('Shipping Region'),
'region_id' => Mage::helper('salesrule')->__('Shipping State/Province'),
'country_id' => Mage::helper('salesrule')->__('Shipping Country'),
);
$this->setAttributeOption($attributes);
return $this;
}
<...>
So if I overwrite this model and add an item to that array I will get the attribute shown in rule definition admin panel. It seems that all these attributes has a matching column in sales_flat_quote_address table except for total_qty and payment_method!
Now the problem is what should I do to have my new attribute be calculated and evaluated in rules processing? should I add a column to this table and update its value upon cart changes?
Any insight on how to do this would be of great value thanks.
I finally managed to accomplish the task and just for future reference I explain the procedure here.
The class mentioned in the question (ie: Mage_SalesRule_Model_Rule_Condition_Address) is the key to the problem. I had to rewrite it and for some odd reason I couldn't get what I needed by extending it so my class extended its parent class (ie: Mage_Rule_Model_Condition_Abstract).
As I said I added my attribute to $attributes like this:
'net_score' => Mage::helper('mymodule')->__('Net Score')
I also modified getInputType() method and declared my attribute as numeric
now what does the trick is the validate() method:
public function validate(Varien_Object $object)
{
$address = $object;
if (!$address instanceof Mage_Sales_Model_Quote_Address) {
if ($object->getQuote()->isVirtual()) {
$address = $object->getQuote()->getBillingAddress();
}
else {
$address = $object->getQuote()->getShippingAddress();
}
}
if ('payment_method' == $this->getAttribute() && ! $address->hasPaymentMethod()) {
$address->setPaymentMethod($object->getQuote()->getPayment()->getMethod());
}
return parent::validate($address);
}
as you can see it prepares an instance of Mage_Sales_Model_Quote_Address and sends it to its parent validate method. you can see that this object ($address) does not have payment_method by default so this method creates one and assigns it to it. So I did the same, simply I added the following code before the return:
if ('net_score' == $this->getAttribute() && ! $address->hasNetScore()) {
$address->setNetScore( /*the logic for retrieving the value*/);
}
and now I can set rules upon this attribute.
Hope that these information saves somebody's time in the future.
Related
I have a custom attribute that calculates the squad name (to make our frontend team lives easier).
This requires a relation to be loaded and even if the attribute is not being called/asked (this happens with spatie query builder, an allowedAppends array on the model being passed to the query builder and a GET param with the required append(s)) it still loads the relationship.
// Model
public function getSquadNameAttribute()
{
$this->loadMissing('slots');
// Note: This model's slots is guaranteed to all have the same squad name (hence the first() on slots).
$firstSlot = $this->slots->first()->loadMissing('shift.squad');
return ($firstSlot) ? $firstSlot->shift->squad->name : null;
}
// Resource
public function toArray($request)
{
return [
'id' => $this->id,
'squad_name' => $this->when(array_key_exists('squad_name', $this->resource->toArray()), $this->squad_name),
'slots' => SlotResource::collection($this->whenLoaded('slots')),
];
}
Note: squad_name does not get returned if it's not being asked in the above example, the relationship is however still being loaded regardless
A possible solution I found was to edit the resource and includes if's but this heavily reduces the readability of the code and I'm personally not a fan.
public function toArray($request)
{
$collection = [
'id' => $this->id,
'slots' => SlotResource::collection($this->whenLoaded('slots')),
];
if (array_key_exists('squad_name', $this->resource->toArray())) {
$collection['squad_name'] = $this->squad_name;
}
return $collection;
}
Is there another way to avoid the relationship being loaded if the attribute is not asked without having spam my resource with multiple if's?
The easiest and most reliable way I have found was to make a function in a helper class that checks this for me.
This way you can also customize it to your needs.
-- RequestHelper class
public static function inAppends(string $value)
{
$appends = strpos(request()->append, ',') !== false ? preg_split('/, ?/', request()->append) : [request()->append];
return in_array($value, $appends);
}
-- Resource
'squad_name' => $this->when(RequestHelper::inAppends('squad_name'), function () {
return $this->squad_name;
}),
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.
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'];
}
}
}
When creating a product I can use the following via the API:
$newProductData = array(
'name' => (string)$stockItem->STOCK_DESC,
'websites' => array(1,2), // array(1,2,3,...)
'short_description' => (string)$stockItem->STOCK_DESC,
'description' => (string)$stockItem->LONG_DESC,
'status' => 1,
'weight' => $stockItem->WEIGHT,
'tax_class_id' => 1,
'categories' => array(3108),
'price' => $stockItem->SELL_PRICE
);
$my_set_id = 9; // Use whatever set_id you want here
$type = 'simple';
$mc = new Mage_Catalog_Model_Product_Api();
$mc->create($type, $my_set_id, $stockItem->STOCK_CODE, $newProductData);
When I look into the $mc->create call I see that it does this:
foreach ($product->getTypeInstance(true)->getEditableAttributes($product) as $attribute) {
}
which indicates there is a list of attributes which can be edited against an object.
How do I find these? Is there a specific place this information is found?
Edit: I just did:
Mage::log($product->getTypeInstance(true)->getEditableAttributes($product));
and had a look at the results. It seems all the editable attributes are there which can be found under [attribute_code] => but I would still like a better method of knowing where to look to get this list.
This will depend entirely on the attribute set of the product you're trying to edit, and the configuration of each individual attribute. There's no place in the UI that will list these attributes out for you. Your best bet is to run some custom code for your product
$product = Mage::getModel('catalog/product')->load($product_id);
foreach ($product->getTypeInstance(true)->getEditableAttributes($product) as $code=>$attribute)
{
var_dump($code);
}
Here's how to track this information down. If you jump to the getEditableAttributes method
#File: app/code/core/Mage/Catalog/Model/Product/Type/Abstract.php
public function getEditableAttributes($product = null)
{
$cacheKey = '_cache_editable_attributes';
if (!$this->getProduct($product)->hasData($cacheKey)) {
$editableAttributes = array();
foreach ($this->getSetAttributes($product) as $attributeCode => $attribute) {
if (!is_array($attribute->getApplyTo())
|| count($attribute->getApplyTo())==0
|| in_array($this->getProduct($product)->getTypeId(), $attribute->getApplyTo())) {
$editableAttributes[$attributeCode] = $attribute;
}
}
$this->getProduct($product)->setData($cacheKey, $editableAttributes);
}
return $this->getProduct($product)->getData($cacheKey);
}
You can see that this method gets a list of all the attributes set on a particular product.(i.e. All the attributes that are a member of the product's attribute set). Once it has this list, it goes through each and checks if its apply_to property matches the type id of the current product.
The Apply To attribute is set at
Catalog -> Attributes -> Manage Attributes -> [Pick Attribute]
This form field updates the database table catalog_eav_attribute. If you run the following query you can see examples of this value as is stored
select attribute_id, apply_to from catalog_eav_attribute where apply_to is NOT NULL;
75 simple,configurable,virtual,bundle,downloadable
76 simple,configurable,virtual,bundle,downloadable
77 simple,configurable,virtual,bundle,downloadable
78 simple,configurable,virtual,bundle,downloadable
79 virtual,downloadable
So, get your product's attribute set. Get a list of attributes in that set. Compare the value of the attribute's apply_to field vs. the value of your product's type_id. That will let you build a list of these attributes.
I'm using Symfony 2.1 forms with PropelBundle and I'm trying to refactor a form that had a drop-down list of objects (to select from) to instead use a jquery autocomplete field (working with AJAX). For the dropdown list I was using the following code (which worked perfectly for the drop-down) in my form type:
$builder->add('books', 'collection', array(
'type' => 'model',
'options' => array(
'class' => 'MyVendor\MyBundle\Model\Book',
'property' => 'title',
),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => false,
));
For the sake of giving a little context, let's say we are creating a new "Reader" object and that we would like to select the Reader's favorite books from a list of available "Book" objects. A collection type is used so that many "favorite books" can be selected in the new "Reader" form. Now, I would like to change the above to use autocomplete. For doing so, I tried to implement a Data Transformer to be able to get a Book object from a simple text field that could be used for the Autocomplete function to pass the Book ID as indicated in the answer to this Question. However, I was not able to figure out how to make the Data Transformer work with a collection type and Propel classes. I created a BookToIdTransformer class as indicated in the Symfony Cookbook and tried the following in the "ReaderType" file:
$transformer = new BookToIdTransformer();
$builder->add(
$builder->create('books', 'collection', array(
'type' => 'text',
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => false,
))->addModelTransformer($transformer)
);
With the above, I get a "Call to undefined method: getId" exception (apparently the Transformer expects a PropelCollection of Books, not a single Book object..). Does anyone know how to go about it? or let me know if there are other ways to implement the autocomplete in Symfony using Propel and allowing for selecting multiple objects (e.g. a collection of books)?
The solution I ultimately went for is slightly different from my previous answer. I ended up using a "text" field type instead of a "collection" field type in my "ReaderType" form, since I ended up using the Loopj Tokeninput jQuery plugin which allows for selecting multiple objects (e.g. "Favorite Book") to associate to my main object (e.g. "Reader" object) in the form. Considering that, I created a "Data Transformer" for transforming the objects' ids passed (in a comma separated list in the text field) into a Propel Object Collection. The code is shared as follows, including a sample ajax object controller.
The key part of the "ReaderType" form looks as follows:
$transformer = new BooksToIdsTransformer();
$builder->add(
$builder->create('books', 'text', array(
'required' => false,
))->addModelTransformer($transformer)
);
The "Data Transformer" file looks like this:
// src/MyVendor/MyBundle/Form/DataTransformer/BooksToIdsTransformer.php
namespace MyVendor\MyBundle\Form\DataTransformer;
use \PropelObjectCollection;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use MyVendor\MyBundle\Model\BookQuery;
class BooksToIdsTransformer implements DataTransformerInterface
{
public function transform($books)
{
if (null === $books) {
return "";
}
if (!$books instanceof PropelObjectCollection) {
throw new UnexpectedTypeException($books, '\PropelObjectCollection');
}
$idsArray = array();
foreach ($books as $book) {
$idsArray[] = $book->getId();
}
$ids = implode(",", $idsArray);
return $ids;
}
public function reverseTransform($ids)
{
$books = new PropelObjectCollection();
if ('' === $ids || null === $ids) {
return $books;
}
if (!is_string($ids)) {
throw new UnexpectedTypeException($ids, 'string');
}
$idsArray = explode(",", $ids);
$idsArray = array_filter ($idsArray, 'is_numeric');
foreach ($idsArray as $id) {
$books->append(BookQuery::create()->findOneById($id));
}
return $books;
}
}
The ajax controller that returns a json collection of "books" to the Tokeninput autocomplete function is as follows:
namespace MyVendor\MyBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use MyVendor\MyBundle\Model\BookQuery;
class ClassAjaxController extends Controller
{
public function bookAction(Request $request)
{
$value = $request->get('q');
$books = BookQuery::create()->findByName('%'.$value.'%');
$json = array();
foreach ($books as $book) {
$json[] = array(
'id' => $book->getId(),
'name' => $book->getName()
);
}
$response = new Response();
$response->setContent(json_encode($json));
return $response;
}
}
And finally, the router in the "routing.yml" file:
ajax_book:
pattern: /ajax_book
defaults: { _controller: MySiteBundle:ClassAjax:book }