I have some forms in Yii using the following to get lists of data from related tables in the form of a drop down:
dropDownList(CHtml::listData(Company::model()->findAll(array('order' => 'company ASC'))));
This works, but that means for every drop down list (which theres a lot of) I'm putting this array('order' => 'company ASC' in every one.
Is this the best way to do it? Is there not a way to get this data using the model relations(), and specifying the order within the relation?
I believe that the correct way to do this is by using scopes.
You can define any number of scopes that order the result set and use them like so:
Company::model()->scopeName()->findAll();
If your application always requires companies to be fetched in a sorted order, you can even define a default scope in your model class:
public function defaultScope() {
return array('order' => 'company ASC');
}
This will result in every call to Company::model()->findAll(); returning sorted results.
I usually add an opts() methods to each model that could be used as source for a dropdown:
class Company extends CActiveRecord
{
// ...
public static opts()
{
$opts = array();
foreach(self::model()->findAll(array('order'=>'name ASC')) as $model)
$opts[$model->id] = $model->name;
return $opts;
}
It's used like this
echo $form->dropDownList($user, 'company_id', Company::opts());
If you need the same options several times on a page, you could even "cache" the result in a private static class variable or use DAO to fetch the list data in a more efficient way.
Related
I'm currently struggling with retrieving data towards a parent model. I'll drop my database, classes, and things I've tried before.
I have 4 tables: sales_orders, products, work_orders, and product_sales_order (pivot table between sales_orders and products).
SalesOrder.php
class SalesOrder extends Model
{
public function products()
{
return $this->belongsToMany(Product::class)
->using(ProductSalesOrder::class)
->withPivot(['qty', 'price']);
}
}
ProductSalesOrder.php
class ProductSalesOrder extends Pivot
{
public function work_orders()
{
return $this->hasMany(WorkOrder::class);
}
public function getSubTotalAttribute()
{
return $this->qty* $this->price;
}
}
WorkOrder.php
class WorkOrder extends Model
{
public function product_sales_order()
{
return $this->belongsTo(ProductSalesOrder::class);
}
public function sales_order()
{
return $this->hasManyThrough(
ProductSalesOrder::class,
SalesOrder::class
);
}
}
So, what I want to retrieve sales order data from work order since both tables don't have direct relationship and have to go through pivot table and that is product sales order. I've tried hasOneThrough and hasManyThrough but it cast an error unknown column. I understand that error and not possible to use that eloquent function.
Is it possible to retrieve that sales order data using eloquent function from WorkOrder.php ?
You cannot achieve what you want using hasOneThrough as it goes from a table that has no ID related to the intermediate model.
In your example you are doing "the inverse" of hasOneThrough, as you are going from a model that has the ID of the intermediate model in itself, and the intermediate model has the ID of your final model. The documentation shows clearly that hasOneThrough is used exactly for the inverse.
So you still should be able to fix this, and use a normal relation as you have the sales_orders_id in your model SuratPerintahKerja, so you can use a normal relation like belongsTo to get just one SalesOrder and define it like this:
public function salesOrder()
{
return $this->belongsTo(SalesOrder::class, 'sale_orders_id');
}
If you want to get many SalesOrders (if that makes sense for your logic), then you should just run a simple query like:
public function salesOrders()
{
return $this->query()
->where('sale_orders_id', $this->sale_orders_id)
->get();
}
Have in mind that:
I have renamed your method from sales_order to salesOrder (follow camel case as that is the Laravel standard...).
I have renamed your method from sales_order to salesOrders for the second code as it will return more than 1, hence a collection, but the first one just works with one model at a time.
I see you use sale_orders_id, but it should be sales_order_id, have that in mind, because any relation will try to use sales_order_id instead of sale_orders_id, again, stick to the standards... (this is why the first code needs more parameters instead of just the model).
All pivot tables would still need to have id as primary and auto incremental, instead of having the id of each related model as primary... Because in SuratPerintahKerja you want to reference the pivot table ProdukSalesOrder but it has to use both produks_id (should have been produk_id singular) and sale_orders_id (should have been sales_order_id). So if you were able to use something like produk_sales_order_id, you could be able to have better references for relations.
You can see that I am using $this->query(), I am just doing this to only return a new query and not use anything it has as filters on itself. I you still want to use current filters (like where and stuff), remove ->query() and directly use the first where. If you also want to add ->where('produks_id', $this->produks_id) that is valid and doesn't matter the order. But if you do so, I am not sure if you would get just one result, so ->get() makes no sense, it should be ->first() and also the method's name should be salesOrder.
Sorry for this 6 tip/step, but super personal recommendation, always write code in English and do not write both languages at the same time like produks and sales orders, stick to one language, preferrably English as everyone will understand it out of the box. I had to translate some things so I can understand what is the purpose of each table.
If you have any questions or some of my code does not work, please tell me in the comments of this answer so I can help you work it out.
Edit:
After you have followed my steps and changed everything to English and modified the database, this is my new code:
First, edit ProductSalesOrder and add this method:
public function sales_order()
{
return $this->belongsTo(SalesOrder::class);
}
This will allow us to use relations of relations.
Then, have WorkOrder as my code:
public function sales_order()
{
return $this->query()->with('product_sales_order.sales_order')->first();
}
first should get you a ProductSalesOrder, but then you can access ->sales_order and that will be a model.
Remember that if any of this does not work, change all the names to camelCase instead of kebab_case.
I have a multi-language website and what I want to do is displaying different content from different tables (lang_en, lang_es, lang_fr.. etc)
I've tried something like this: [*1]:
[*1]:
// ContentController.php
public function content_bylang($id)
{
$table = 'lang_'.Config::get('app.locale'); // app.locale=["en", "es", ..]
$data = \DB::table($table)->where('id', $id)->get();
return view('content', ['data' => $data]);
}
When I do it this way, it works but the way I do it really doesn't seem so effective.
Is there any other way for handling this kind of calls?
--
Things I can't do:
I can't add each content inside the resources/lang/x.php file
if your many table has same column can use Model and override the Model's constructor
parent::__construct();
$this->table = 'lang_'.Config::get('app.locale');
so next time in your controller only need to call the model without extra configuration and pass it to the view
We have a COMMON database and then tenant databases for each organization that uses our application. We have base values in the COMMON database for some tables e.g.
COMMON.widgets. Then in the tenant databases, IF a table called modified_widgets exists and has values, they are merged with the COMMON.widgets table.
Right now we are doing this in controllers along the lines of:
public function index(Request $request)
{
$widgets = Widget::where('active', '1')->orderBy('name')->get();
if(Schema::connection('tenant')->hasTable('modified_widgets')) {
$modified = ModifiedWidget::where('active', '1')->get();
$merged = $widgets->merge($modified);
$merged = array_values(array_sort($merged, function ($value) {
return $value['name'];
}));
return $merged;
}
return $countries;
}
As you can see, we have model for each table and this works OK. We get the expected results for GET requests like this from controllers, but we'd like to merge at the Laravel MODEL level if possible. That way id's are linked to the correct tables and such when populating forms with these values. The merge means the same id can exist in BOTH tables. We ALWAYS want to act on the merged data if any exists. So it seems like model level is the place for this, but we'll try any suggestions that help meet the need. Hope that all makes sense.
Can anyone help with this or does anyone have any ideas to try? We've played with overriding model constructors and such, but haven't quite been able to figure this out yet. Any thoughts are appreciated and TIA!
If you put this functionality in Widget model you will get 2x times of queries. You need to think about Widget as an instance, what I am trying to say is that current approach does 2 queries minimum and +1 if tenant has modified_widgets table. Now imagine you do this inside a model, each Widget instance will pull in, in a best case scenario its equivalent from different database, so for bunch of Widgets you will do 1 (->all())+n (n = number of ModifiedWidgets) queries - because each Widget instance will pull its own mirror if it exists, no eager load is possible.
You can improve your code with following:
$widgets = Widget::where('active', '1')->orderBy('name')->get();
if(Schema::connection('tenant')->hasTable('modified_widgets')) {
$modified = ModifiedWidget::where('active', '1')->whereIn('id', $widgets->pluck('id'))->get(); // remove whereIn if thats not the case
return $widgets->merge($modified)->unique()->sortBy('name');
}
return $widgets;
OK, here is what we came up with.
We now use a single model and the table names MUST be the same in both databases (setTable does not seem to work even though in exists in the Database/Eloquent/Model base source code - that may be why it's not documented). Anyway = just use a regular model and make sure the tables are identical (or at least the fields you are using are):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Widget extends Model
{
}
Then we have a generic 'merge controller' where the model and optional sort are passed in the request (we hard coded the 'where' and key here, but they could be made dynamic too). NOTE THIS WILL NOT WORK WITH STATIC METHODS THAT CREATE NEW INSTANCES such as $model::all() so you need to use $model->get() in that case:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class MergeController extends Controller
{
public function index(Request $request)
{
//TODO: add some validations to ensure model is provided
$model = app("App\\Models\\{$request['model']}");
$sort = $request['sort'] ? $request['sort'] : 'id';
$src_collection = $model->where('active', '1')->orderBy('name')->get();
// we setup the tenants connection elsewhere, but use it here
if(Schema::connection('tenant')->hasTable($model->getTable())) {
$model->setConnection('tenant');
$tenant_collection = $model->get()->where('active', '1');
$src_collection = $src_collection->keyBy('id')->merge($tenant_collection->keyBy('id'))->sortBy('name');
}
return $src_collection;
}
}
If you dd($src_collection); before returning it it, you will see the connection is correct for each row (depending on data in the tables). If you update a row:
$test = $src_collection->find(2); // this is a row from the tenant db in our data
$test->name = 'Test';
$test->save();
$test2 = $src_collection->find(1); // this is a row from the tenant db in our data
$test2->name = 'Test2'; // this is a row from the COMMON db in our data
$test2->save();
dd($src_collection);
You will see the correct data is updated no matter which table the row(s) came from.
This results in each tenant being able to optionally override and/or add to base table data without effecting the base table data itself or other tenants while minimizing data duplication thus easing maintenance (obviously the table data and population is managed elsewhere just like any other table). If the tenant has no overrides then the base table data is returned. The merge and custom collection stuff have minimal documentation, so this took some time to figure out. Hope this helps someone else some day!
I have these two database tables:
locations
id
name
users
id
location_id
last_name
first_name
I also have User and Location class, they both extends Model and contain some custom methods. For example, there is a get_full_name() method in the User class.
I am using the following codes to load the data,
$this->db->select('users.id, locations.name as location_name, users.last_name, users.first_name');
$this->db->from('users');
$this->db->join('locations', 'users.location_id = location.id');
$query = $this->db->get();
$users= $query->custom_result_object('User'); //now $users is an array of User object
The custom_result_object is a built-in but undocumented function in Codeigniter. It accepts a string of class name and will use it to create objects of that class.
With the above codes, I can access the data like this,
foreach($users as $user)
{
echo $user->id;
echo $user->get_full_name();
echo $user->location_name;
}
But as you can see, location_name is now a field of the User object, what I want is the User object has a field of Location object that allows me to use it like this,
foreach($users as $user)
{
echo $user->id;
echo $user->get_full_name();
echo $user->location->name; // Location is an object
}
I don't want to use DataMapper ORM or any other ORMs, and would also like to avoid the N+1 query performance issue.
How can I do this with minimal amount of codes?
Note: I made up this example just for the demonstration purpose, in my real case, there are quite a lot of tables and classes (which has custom methods defined on them).
Many thanks to you all.
Have you considered the following?
class User {
var location;
public function location()
{
if ( empty($this->location) )
{
$ci =& get_instance();
$this->location = $ci->db->get_where('locations', array('id' => $this->location_id))->row();
}
return $this->location;
}
}
This way, you completely avoid the overhead of loading the location's table unless you need the data, in which case you cache it inside the object for future use.
I had questioned the same for myself but found the simplest way was to iterate through each result (in your case, user) and select the corresponding child (location) and set to the field in the parent object (user->location).
The only alternative way I can think of would involve editing (or extending and creating your own db set) the db_result class to use getters/setters instead of direct fields - perhaps with call_user_func_array(). At that point, you could code your setId() function in the user model to set the $location field upon receiving the userId/foreign key.
get location id from table user, use it's number to grab from table locations by id a object. and put in it.
My Problem: Linked to my Employees table I've got an Address table containing a virtual field called full_name (I guess you can imagine by yourself what it does). I added the Containable Behaviour and this function
function beforeFind() {
$this->contain('Address.full_name');
}
to my Employees model, so that I don't have to call $this->contain(..) in every single controller action (I need the full_name field in pretty every action). BUT id doesn't work then if the controller action does just a $this->Employee->find('all') (or read(..). Contrary, it works if
The controller action uses $this->paginate(); instead
$this->Employee->contain('Address.full_name'); gets called before the $this->Employee->find('all'); call. I can't imagine the cause for this because after this explicit contain(..) call, contain(..) gets called again by the Model callback function beforeFind(), as a debug proofed which I inserted into the cake/libs/models/behaviours/containable.php:contain() function *cough*.
As far as I recall, a contain() statement only works once, for the query operation immediately following it. Subsequent queries will require their own contain() statement e.g.
$this->Employee->contain('Address.full_name');
$this->Employee->find('all'); //first find
// returns all of Employee + Address.full_name
$this->Employee->find('all'); //second find
// returns all of Employee + all of Address + all of any other associated tables.
I don't recommend using contain() in beforeFind() as it is intended to modify specific returns. It soon becomes second nature to use it before each query where you will then have fine control of the data returned.
If you have a widespread requirement for a limited return, you can set that up in the associations on the model.
1) Your Model beforeFind() should accept a param $queryData and return true if the find() should be executed, or false if it should abort beforeFind. Generally the beforeFind($queryData) method will modify the $queryData array and return it.
function beforeFind($queryData) {
// Modify $queryData here if you want
return $queryData
}
2) Trying to maintain a persistant $contain is a bit strange. Containable obviously contains assocations so that extra/additional information is fetched. Your virtual field should be returned in a normal find() operation. If you want to restrict the fields which are returned you should define those either in the Model or in the Model association
var $belongsTo = array(
'Employee' => array(
'className' => 'Employee',
'fields' => array('Employee.id', 'Employee.full_name'),
'conditions' => array()
)
);
Containable will rebind associations quickly for you, but you should instead define default fields/conditions through the Model association ($belongsTo, $hasMany, $hasOne etc)
Another alternative is to actually create Model methods which reflect the data you are trying to fetch, a very basic example:
function activeEmployees() {
$contain = array('Address');
$conditions array('Employee.started' => '2010-09-01');
$this->find('all', array('conditions' => $conditions, 'contain' => $contain));
}
And then call these convienience methods from the Controller just like you would a find
$this->Employee->activeEmployees();
You could also choose to pass a param to Employee::activeEmployees(); which is an array of additional $conditions or $contain options which are merged with the standard options.