Laravel - Get value associated with product - php

In my project, I'm working with polymorphic relations which I find very hard to understand. Do have in mind that I am a beginner in programming. My Database looks something like this:
Themes
id - integer
composer_package - string
name - string
Plugins
id - integer
composer_package - string
name - string
Products
id - integer
name - string
...
productable_id - integer
productable_type - string
In the store method below. I am getting the ID of the selected theme. Then I find that theme by doing $theme = Product::find($selectedTheme);. The $selectedTheme is the ID of the theme. I have an Array which is called predefinedArray which contains all the fillable fields of theme. Then It puts all the values that that specific theme has in a session called chosen_theme.
public function store(Request $request)
{
$selectedTheme = null;
foreach($request->input('theme') as $key => $value) {
if($value === 'selected') {
$selectedTheme = $key;
}
}
$theme = Product::find($selectedTheme);
foreach($this->predefinedArray as $value) {
$request->session()->put('chosen_theme.' . $value, $theme->$value);
}
$data = $request->session()->all();
return redirect('plugins');
}
The theme is a product. I need to get the composer_package that is associated with it and put it in the request. Say, for instance, I find a theme with the ID 20, This theme's productable_id is 5. Then I need to get the composer_package in the Themes table where the ID is 5 and put it inside the request. How can I achieve that? The array currently looks like this:
As you can see, The composer_package field is empty, This needs to be filled with the composer_package that is associated with the selected product.
My models look like this:
Product
public function productable()
{
return $this->morphTo();
}
public function order_items()
{
return $this->hasMany(Orderitems::class);
}
Theme
public function webshops()
{
return $this->hasMany(Webshop::class);
}
public function products()
{
return $this->morphMany(Product::class, 'productable');
}
How can I achieve this? Thanks in advance!

When you are doing this
$theme = Product::find($selectedTheme);
You are loading the data from the product table. The composer_package field is however not stored in the product table, but in the morphed row from the theme / plugin table.
To access that value you need to do $theme->productable->composer_package
A quick and dirty way of doing that might be this:
foreach($this->predefinedArray as $value) {
$request->session()->put('chosen_theme.' . $value, $theme->$value);
}
$request->session()->put('chosen_theme.composer_package', $theme->productable->composer_package);

Related

Eloquent Collections, how to modify the data of each registry contained in results?

I have the following situation, Im trying to modify the price of products displayed in a platform.
Everything works ok for only 1 product (eg: product view) but I dont know what I have to do in order to modify the price of each product in an eloquent collection.
this is the code in my app:
ProductRepository.php:
public function CalcPrice($product){
$x = $product->price; //eg 5
$y = 4;
$amount= $x + $y;
return $amount;
}
For the details view of each product inside ProductController I have the following code and everything works perfect:
public function details($id){
$product = $this->product->getProductById($id);
$productprice = $this->product->getCalcPrice($product = $product);
return view('products.view',compact('product','productprice'))
}
On the other hand, my idea is to use the code contained in ProductRepository.php function CalcPrice in a collection.
My main doubt is what do I have to do, because in a collection probably I can have a variable $category in order to retrieve all products in a category, but I will not have a variable for each $product (for eg: a $productid like in details).
What can I do in order to eg:
modify each product price contained in a collection of a category
using CalcPrice function code?
eg: of code:
productrepository.php
public function AllProductsInCategory($catid)
{
return App\Product::where('categoryid', $catid)
->get();
}
but each product displaying their ($product->price + 4) as CalcPrice performs. thanks!.
You can achieve this by defining an attribute accessor on model and append it to model. This way it would be available for you on each instance like its other attributes.
As Taylor Otwell mentioned here, "This is intentional and for performance reasons." However there is an easy way to achieve this, say you have model named Product;
class Product extends Eloquent {
protected $appends = array('calc_price');
public function getCalcPriceAttribute()
{
//if you want to call your method for some reason
return $this->getCalcPrice($this);
// Otherwise more clean way would be something like this
// return $this->price + 4 // un-comment if you don't want to call getCalcPrice() method
}
}
Now you can access calculated price on each $product by simply calling $product->calc_price.

Yii2: Assigning dynamic properties to model and displaying them in a grid

To keep things short, the application itself is really simple, having only three tables.
Product:
id
name
Attribute
id
name
slug
Product Attribute
id
attribute_id
product_id
value
As you may guess the attributes may be totally random, with any content and there might be any number of them. Every product has the same set of global attributes assigned. Some are empty, some are filled.
What I want to do is displaying them in a standard Gridview, the same way as if it was a normal model, with dynamic-preassigned columns and values. Basically speaking - the attributes should serve as the columns.
I've tried to extend the main Product Model and use ActiveDataProvider class on it, but no avail, I'm getting the custom attributes values repeated for each row, as if something was missing. Here's the class I've created basing on another question from SO.
namespace common\models;
use Yii;
use common\models\Attribute;
use common\models\Product;
class ProductDynamic extends Product {
public function init() {
parent::init();
$attrExists = ProductAttribute::find()->select(['slug','value'])->joinWith(['attributeModel'])->all();
if ($attrExists) {
foreach ($attrExists as $at) {
$this->dynamicFields[$at['slug']] = $at['value'];
}
}
}
private $dynamicFields = [];
public function __get($name) {
if (array_key_exists($name, $this->dynamicFields))
return $this->dynamicFields[$name];
else
return parent::__get($name);
}
public function __set($name, $value) {
if (array_key_exists($name, $this->dynamicFields))
$this->dynamicFields[$name] = $value;
else
parent::__set($name, $value);
}
public function rules() {
return array_merge(parent::rules, $this->dynamicRules);
}
}
My question is - how would you do it? How to assign properties to a model so they act as a standard database-loaded properties usable by ActiveDataProvider? I think I need to retrieve the ProductAttributes some other way so they are not repeated. See below.
Ok, i think i got your idea...
So, I think you could generate an attribute in the model of Product (we will call it ProductAttributeArr) which would be an array, then retrieve the attributes from the Attribute table in the database according to Product Attribute table and store them in ProductAttributeArr.
Then you could create a function that dynamically generates the parameters for the GridView base on the content of ProductAttributeArr. This way it should work.
Answering it myself since there's no feedback and after some more digging i've found the quickest, but maybe not the nicest solution.
Since the properties in main Product model are added thanks to the ProductDynamic class I have added the following in Product to assign correct column values. For sure, there must be another, easier way, if you know any feel free to comment.
public function afterFind() {
$attrs = ProductAttribute::find()->select(['slug', 'value'])->joinWith(['attributeModel'])->where(['product_id' => $this->id])->all();
foreach ($attrs as $a) {
$this->{$a['slug']} = $a['value'];
}
}

Get next / prev element in Eloquent

I have a model for Product and Catalogue. They have many to many relationship, therefore I am using a pivot table called 'items'. I have a route:
/something/something/{catalogue}/{product}
I am passing the product model and catalogue model to the view, which outputs the product information and catalogue information - pretty straight forward. Now I need to have 2 buttons - 'next' and 'previous' to navigate through products in the catalogue.
So one way to do this, I thought I would create 2 methods on the Catalogue model that would accept the current product model and return next / prev model based on ID:
public function prevProduct($product){
$prevProdId = Product::where('id', '<', $product->id)->max('id');
return Product::find($prevProdId);
}
public function nextProduct($product){
$nextProdId = Product::where('id', '>', $product->id)->min('id');
return Product::find($nextProdId);
}
Now this works fine, but as you can see, it retrieves the next product and previous product from the Product table in the database, not the catalogue.
To get all the products in the catalogue, can be done like so: $this->items (on Catalogue model) or $catalogue->items (from view).
I somehow need to get next / prev items from the Catalogue, but can't figure out how. Any suggestions would be much appreciated.
You can filter Collection, like this:
$next = $item->id + 1;
$catalogue->filter(function($item) use ($next ) {
return $item->id == $next;
})->first();
I use global method added to Collection class:
/**
* Returns first object from collection which meets attribute criteria
*
* #param string $attributeName name of attribute we are looking for
* #param string $attributeValue value of attribute that have to be met
* #return \Illuminate\Database\Eloquent\Collection
*/
public function findByAttribute($attributeName, $attributeValue)
{
return $this->filter(function($item) use ($attributeName, $attributeValue) {
return $item->{$attributeName} == $attributeValue;
})->first();
}
In this case I would use this method this way:
$catalogue->items->findByAttribute('id', $next);
In both examples I assume You have $item object to reffer to.
You can use Pagination
Catalogue::first()->items()->paginate(1);
Actually, it was much simpler. This did the trick:
$nextProdId = $this->items()->where('product_id', '>', $product->id)->min('product_id');
return Product::find($nextProdId);
I had to add () after 'items' to enable 'where' clause and make it searchable basically.

Doctrine Events, update persisted entity

i wrote an Importer script, which read entries from an csv file,
and iterate the rows. To handle big files without performance loss,
i insert new data within doctrine batch(bulks).
My problem at the moment is, i have an "Category" entity, which should be expanded
only within new entries. So i have to check if entries are available given category names.
My first question is, i've read that doctrines "prePersist" event will be called on call
"$entityManager->persist()" and inside the "flush" method (http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#prepersist).
So how can i check if the event was inside an flush?
The next thing, how can i update the actually entity within the identity datas?
I try to set the id, but without any effect.
<?php
/**
* #return \Doctrine\Commong\Collections\ArrayCollection
*/
public function getCategories()
{
if (null === $this->categories) {
$this->categories = $this->getServiceCategory()->findAll();
}
return $this->categories;
}
public function prePersist(LifecycleEventArgs $event)
{
$entity = $event->getEntity();
$objectManager = $event->getObjectManager();
if ($entity instanceof \Application\Entity\Category) {
$categories = $this->getCategories();
$entityCriteria = // buildCriteria from Entity;
$matched = $categories->matching($entityCriteria);
if ($matched->count() > 0) {
$entity->setId($matched->first()->getId();
}
}
}
So, here i dont know how to update the persisted categorie entity?
Is this the right event, or should be an other event a better solution for my situation?
I developed the import within zf2 and doctrine2.
regards
First I would recommend to use DQL instead of ORM entities within you import script, because it makes you code much more simple.
Your import process increases the performance, if you first (1.) read all existing "Categories" from yor database, keep them within a member variable and second (2.) iterate each csv row and compare its category foreign key with the initially read set of categories.
If the category already exists in you database, create a new entity row with the existing corresponding foreign key, else create a new category and create a new entity row associated to the new category.
<?php
// read categories from database
$categories = "..."; /* e.g. array('A' => 1,
'B' => 2, ...); */
// ...
// Iterate each csv row
foreach($csvRows as $csvRow) {
// check category name
if(!array_key_exists($csvRow['category']), $categories) {
// Create new Category
// Remember id of the new created category
$categoryId = "...";
} else {
// Use already existing category id
$categoryId = $categories[$csvRow['category']];
}
// Create new csv row entity with category foreign key
// ...
}
?>

Custom is_unique_logical_key - validation or callback?

Can I please have a design suggestion for the following problem:
I am using Codeigniter/Grocery_CRUD.
My system is multi tenanted - different autonomous sites - within the same client. I have quite a few instances of tables that have unique logical keys. One such table structure is:
equip_items
id (pk)
equip_type_id (fk to equip_types)
site_id (fk to sites)
name
Where (equip_type_id, site_id, name) together are a unique key in my db.
The issues is that when using a grocery_CRUD form to add or edit a record that breaks this database rule - the add or edit fails (due to the constraints in the db) but I get no feedback.
I need a variation on the is_unique form_validation rule by which I can specify the field*s* that must be unique.
The issues:
How to specify the rule? set_rules() is for a given field and I have multiple fields that the rule will apply to. Does that mean I should abandon the Form_validation pattern? Or do I follow the 'matches' rule pattern and somehow point to the other fields?
Perhaps a callback function would be better but this would mean writing a custom function in each model where I have this problem at last count this is 9 tables. It seems far better to do this in one place (extending form_validation).
Am I missing something already in codeigniter or grocery_CRUD that has already solved this problem?
Any suggestion/advice you might have would be appreciated.
EDIT:
Actually it appears the solution Johnny provided does not quite hit the mark - it enforces each field in unique_fields() being independently unique - the same as setting is_unique() on each one. My problem is that in my scenario those fields are a composite unique key (but not the primary key). I don't know if it is significant but further to the original problem statement: 1) site_id is a 'hidden' field_type - I don't want my users concerned they are on a different site so I'm dealing with site_id behind the scenes. 2) Same deal with an equip_status_id attribute (not part of the unique key). And 3) I have set_relations() on all these foreign key attributes and grocery_CRUD kindly deals with nice drop downs for me.
EDIT 2
I have solved this using a callback.
UPDATE: This code is now part of grocery CRUD version >= 1.4 and you don't need to use an extension anymore. For more see the documentation for unique_fields
I will try to explain it as easy as I can:
1. First of all for those who have grocery CRUD lower or equal to 1.3.3 has to use this small change: https://github.com/scoumbourdis/grocery-crud/commit/96ddc991a6ae500ba62303a321be42d75fb82cb2
2. Second create a file named grocery_crud_extended.php at application/libraries
3. Copy the below code at your file application/libraries/grocery_crud_extended.php
<?php
class grocery_CRUD_extended extends grocery_CRUD
{
protected $_unique_fields = array();
public function unique_fields()
{
$args = func_get_args();
if(isset($args[0]) && is_array($args[0]))
{
$args = $args[0];
}
$this->_unique_fields = $args;
return $this;
}
protected function db_insert_validation()
{
$validation_result = (object)array('success'=>false);
$field_types = $this->get_field_types();
$unique_fields = $this->_unique_fields;
$add_fields = $this->get_add_fields();
if(!empty($unique_fields))
{
$form_validation = $this->form_validation();
foreach($add_fields as $add_field)
{
$field_name = $add_field->field_name;
if(in_array( $field_name, $unique_fields) )
{
$form_validation->set_rules( $field_name,
$field_types[$field_name]->display_as,
'is_unique['.$this->basic_db_table.'.'.$field_name.']');
}
}
if(!$form_validation->run())
{
$validation_result->error_message = $form_validation->error_string();
$validation_result->error_fields = $form_validation->_error_array;
return $validation_result;
}
}
return parent::db_insert_validation();
}
protected function db_update_validation()
{
$validation_result = (object)array('success'=>false);
$field_types = $this->get_field_types();
$unique_fields = $this->_unique_fields;
$add_fields = $this->get_add_fields();
if(!empty($unique_fields))
{
$form_validation = $this->form_validation();
$form_validation_check = false;
foreach($add_fields as $add_field)
{
$field_name = $add_field->field_name;
if(in_array( $field_name, $unique_fields) )
{
$state_info = $this->getStateInfo();
$primary_key = $this->get_primary_key();
$field_name_value = $_POST[$field_name];
$ci = &get_instance();
$previous_field_name_value =
$ci->db->where($primary_key,$state_info->primary_key)
->get($this->basic_db_table)->row()->$field_name;
if(!empty($previous_field_name_value) && $previous_field_name_value != $field_name_value) {
$form_validation->set_rules( $field_name,
$field_types[$field_name]->display_as,
'is_unique['.$this->basic_db_table.'.'.$field_name.']');
$form_validation_check = true;
}
}
}
if($form_validation_check && !$form_validation->run())
{
$validation_result->error_message = $form_validation->error_string();
$validation_result->error_fields = $form_validation->_error_array;
return $validation_result;
}
}
return parent::db_update_validation();
}
}
4. Now you will simply have to load the grocery_CRUD_extended like that:
$this->load->library('grocery_CRUD');
$this->load->library('grocery_CRUD_extended');
and then use the:
$crud = new grocery_CRUD_extended();
instead of:
$crud = new grocery_CRUD();
5. Now you can simply have the unique_fields that it works like this:
$crud->unique_fields('field_name1','field_name2','field_name3');
In your case:
$crud->unique_fields('equip_type_id','site_id');
Pretty easy right?
This is checking if the field is unique or not without actually change the core of grocery CRUD. You can simply use the grocery_CRUD_extended instead of grocery_CRUD and update grocery CRUD library as normal. As I am the author of the library I will try to include this to grocery CRUD version 1.4, so you will not have to use the grocery_CRUD_extended in the future.
I have done this using a callback:
$crud->set_rules('name','Name','callback_unique_equip_item_check['.$this->uri->segment(4).']');
function unique_equip_item_check($str, $edited_id)
{
$var = $this->Equip_Item_model->is_unique_except(
$edited_id,
$this->input->post('site_id'),
$this->input->post('equip_type_id'),
$this->input->post('name'));
if ($var == FALSE) {
$s = 'You already have an equipment item of this type with this name.';
$this->form_validation->set_message('unique_equip_item_check', $s);
return FALSE;
}
return TRUE;
}

Categories