I'm trying to create a system more dynamic for my users, implementing a way of they create each necessary information when they needed. (Sorry if my english is weak)
First, I have a table named User, like this:
User:
id | name | login | password
1 | Rose | rose.120 | 897763
2 | John | john.red | 120347
3 | Mark | mark.foo | 385598
and the other table with some info of each user:
User_info:
id | user_id | info_name | value | required | order
6 | 1 | Telephone | 555-4083 | yes | 2
7 | 1 | Email | rose.120#g.com | yes | 1
8 | 1 | Another E-mail | rose.mg#fc.com | no | 3
When I get this values from the database, how make an efficient way to set a PHP Object, or an Array of this values.
I tried to make a mysql_query and with this result make a loop in the Object, and for any Row make a WHERE clause to the user id. (I'm using CodeIgniter)
if( $user != false ) $this->getUser();
foreach( $this->user->result() as $row ) {
$this->db->where('user_id', $row->id);
}
$this->db->from('user_info');
...
and when I have this Object I will need to put the User_info rows in the User Object. Correct?
I'm just curious if this is a good way of doing this, or I'm in the wrong way.
I hope that's clear.
Thanks.
Basically, yes. You could set up your model to define a method for retrieving information from both your user_info and user tables, however. Something along these lines:
// in user_model.php
function get_user_with_info() {
$user = $this->getUser();
$user['info'] = $this->db->where('user_id', $row->id)->from('user_info')->row();
return $user;
}
If you are planning to make this query often, you will quickly run into the N+1 problem. A better option in Codeigniter might be to use a table join in a get_users function:
function get_users() {
$this->db->select('*')
$this->db->from('user');
$this->db->join('user_info', 'user.id = user_info.user_id');
$results = $this->db->result();
$users = [];
foreach ($results as $result) {
$user_id = $result['user_id'];
if (!isset($users[$user_id])) {
$users[$user_id] = array(
'id' => $user_id,
'info' => array(
// add info fields from first result
)
);
} else {
$users[$user_id]['info'][] = array(
// add additional info fields
);
}
}
return $users;
}
You will need to adjust the specifics, but hopefully this gives somewhere to start.
Don't forget, in this way you must working with dynamic arrays in user object. Instead of fields named as "Telephone", "Email" and other, which you can access as $user->Telephone, $user->Email and other (this is a simple and I think a good way), you must create dynamic array with pairs $info_name->$value. And in "User" object you must create additional functionality, which parse each property $info_name. This is more flexible, but more complex.
So I think, you must find golden mean for you between flexibility and complexity.
While using code codeIgniter, make a function in your model file and put your code. Then, in the controller, call this function in the model and send your required parameter. You are using codeigniter then you have to follow the architecture too.
Related
I have an interesting eloquent challenge and my knowledge is not quite there to work it through, hoping you smart folks can give me some guidance. I have read this: Getting just the latest value on a joined table with Eloquent and it is very close to what I want, but I have an extra level of tables in there.
I have two tables like this:
Users have many Assessments. Each Assessment hasOne body_fat_testing (and 4 other hasOnes that I need to use as well).
Each body_fat_testing table has several columns I'm interested (see below) in.
I'd like to get the "best" result from each "grandchild", per user.
So out of all the assessments, the best lean_mass, body_fat_percentage, etc
I have this so far:
$users = User::with(
[
'trainer',
'firstAssessment.body_fat_testing',
'firstAssessment.circumference_measurement',
'firstAssessment.exercise_testing',
'firstAssessment.overhead_squat_movement_screen',
'firstAssessment.push_ups_movement_screen',
'lastAssessment.body_fat_testing',
'lastAssessment.circumference_measurement',
'lastAssessment.exercise_testing',
'lastAssessment.overhead_squat_movement_screen',
'lastAssessment.push_ups_movement_screen',
]
)->find($request->input('user_ids'));
...
(in User.php)
public function lastAssessment()
{
return $this->hasOne('App\Assessment')->latest('assessment_date');
}
public function firstAssessment()
{
return $this->hasOne('App\Assessment')->orderBy('assessment_date');
}
There are about 30 total values I want the "best" of. Any ideas on how to accomplish this without looping through each and every value individually?
assessment table:
+----+---------+-----+--------+---------------+
| id | user_id | age | weight | assessed_date |
+----+---------+-----+--------+---------------+
| 1 | 7 | 24 | 204 | 2019-10-29 |
+----+---------+-----+--------+---------------+
body_fat_testing table:
+----+---------------+-----------+---------------------+
| id | assessment_id | lean_mass | body_fat_percentage |
+----+---------------+-----------+---------------------+
| 1 | 1 | 130.97 | 21.10 |
+----+---------------+-----------+---------------------+
Try this approach (it assumes you have models for your three tables and the relationships between them exist):
First, you'll relate your User model to your BodyFatTesting model, with this:
public function bodyFatTestings() {
return $this->hasManyThrough("App\BodyFatTesting", "App\Assessment");
}
Then, you'll create a secondary method bestBodyFatPercentage like this:
public function bestBodyFatTesting() {
return $this->bodyFatTestings()->max('body_fat_testing.body_fat_percentage');
}
To use it, just get a model in whichever way you prefer and call bestBodyFatTesting().
From there I think you can adapt it to your other "has ones".
You can also do it this way:
public function bestLeanMass()
{
return $this->hasOneThrough('App\BodyFatTesting', 'App\Assessment')
->selectRaw('user_id, max(lean_mass) as aggregate')
->groupBy('user_id');
}
Not sure which way I like better. The former is a bit easier to understand but creates more methods. This one is harder to understand but fewer methods.
I'm trying to inner join a users table to itself using an eloquent model. I've looked everywhere but can't seem to find a solution to this without creating two queries which is what I am currently doing.
A users table has a many to many relationship itself through the pivot table friends
I tried and failed inner joining Users::class to itself. The best I can get at an inner join is by running two queries and seeing if there is an overlap. Thus one person has reached out to the other and vice versa.
friends | users
----------|------
send_id | id
receive_id| name
is_blocked|
sample data & expected result
users.id | name
---------|------
1 | foo
2 | bar
3 | baz
friends
send_id | receive_id | is_blocked
--------|------------|-----------
1 | 2 | 0
2 | 1 | 0
1 | 3 | 0
3 | 1 | 1
2 | 3 | 0
The user should have an eloquent relationship called friends. It should be what you expect comes out of requestedFriends or receivedFriends just joined.
foo->friends
returns `baz`
bar->friends
returns `foo`
baz->friends
returns empty collection
currently using
// User.php
public function requestedFriends()
{
$left = $this->belongsToMany(User::class, 'friends','send_id','receive_id')
->withPivot('is_blocked')
->wherePivot('is_blocked','=', 0)
->withTimestamps();
return $left;
}
public function receivedFriends()
{
$right = $this->belongsToMany(User::class, 'friends','receive_id','send_id')
->withPivot('is_blocked')
->wherePivot('is_blocked','=', 0)
->withTimestamps();
return $right;
}
public function friends()
{
$reqFriends = $this->requestedFriends()->get();
$recFriends = $this->receivedFriends()->get();
$req = explode(",",$recFriends->implode('id', ', '));
$intersect = $reqFriends->whereIn('id', $req);
return $intersect;
}
Research so far
Laravel Many to many self referencing table only works one way -> old question, but still relevant
https://github.com/laravel/framework/issues/441#issuecomment-14213883 -> yep, it works… but one way.
https://laravel.com/docs/5.8/collections#method-wherein
currently the only way I have found to do this in eloquent.
https://laravel.com/docs/5.7/queries#joins -> Ideally I would find a solution using an innerjoin onto itself, but no matter which way I put the id's I couldn't get a solution to work.
A solution would
A solution would inner join a self referencing table using eloquent in laravel 5.7 or 5.8, where a relationship only exists if send_id & receive_id are present on multiple rows in the friends table.
OR
Somehow let the community know that this can't be done.
Thanks in advance!
I have not checked this solution in every detail yet, but I have written a "ManyToMany" Class extending the "BelongsToMany" Class shipped with laravel, which appears to work.
The class basically just overrides the "get" method, duplicating the original query, "inverting" it and just performing a "union" on the original query.
<?php
namespace App\Database\Eloquent\Relations;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class ManyToMany extends BelongsToMany
{
/**
* Execute the query as a "select" statement.
*
* #param array $columns
* #return \Illuminate\Database\Eloquent\Collection
*/
public function get($columns = ['*'])
{
// duplicated from "BelongsToMany"
$builder = $this->query->applyScopes();
$columns = $builder->getQuery()->columns ? [] : $columns;
// Adjustments for "Many to Many on self": do not get the resulting models here directly, but rather
// just set the columns to select and do some adjustments to also select the "inverse" records
$builder->addSelect(
$this->shouldSelect($columns)
);
// backup order directives
$orders = $builder->getQuery()->orders;
$builder->getQuery()->orders = [];
// clone the original query
$query2 = clone($this->query);
// determine the columns to select - same as in original query, but with inverted pivot key names
$query2->select(
$this->shouldSelectInverse( $columns )
);
// remove the inner join and build a new one, this time using the "foreign" pivot key
$query2->getQuery()->joins = array();
$baseTable = $this->related->getTable();
$key = $baseTable.'.'.$this->relatedKey;
$query2->join($this->table, $key, '=', $this->getQualifiedForeignPivotKeyName());
// go through all where conditions and "invert" the one relevant for the inner join
foreach( $query2->getQuery()->wheres as &$where ) {
if(
$where['type'] == 'Basic'
&& $where['column'] == $this->getQualifiedForeignPivotKeyName()
&& $where['operator'] == '='
&& $where['value'] == $this->parent->{$this->parentKey}
) {
$where['column'] = $this->getQualifiedRelatedPivotKeyName();
break;
}
}
// add the duplicated and modified and adjusted query to the original query with union
$builder->getQuery()->union($query2);
// reapply orderings so that they are used for the "union" rather than just the individual queries
foreach($orders as $ord)
$builder->getQuery()->orderBy($ord['column'], $ord['direction']);
// back to "normal" - get the models
$models = $builder->getModels();
$this->hydratePivotRelation($models);
// If we actually found models we will also eager load any relationships that
// have been specified as needing to be eager loaded. This will solve the
// n + 1 query problem for the developer and also increase performance.
if (count($models) > 0) {
$models = $builder->eagerLoadRelations($models);
}
return $this->related->newCollection($models);
}
/**
* Get the select columns for the relation query.
*
* #param array $columns
* #return array
*/
protected function shouldSelectInverse(array $columns = ['*'])
{
if ($columns == ['*']) {
$columns = [$this->related->getTable().'.*'];
}
return array_merge($columns, $this->aliasedPivotColumnsInverse());
}
/**
* Get the pivot columns for the relation.
*
* "pivot_" is prefixed ot each column for easy removal later.
*
* #return array
*/
protected function aliasedPivotColumnsInverse()
{
$collection = collect( $this->pivotColumns )->map(function ($column) {
return $this->table.'.'.$column.' as pivot_'.$column;
});
$collection->prepend(
$this->table.'.'.$this->relatedPivotKey.' as pivot_'.$this->foreignPivotKey
);
$collection->prepend(
$this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->relatedPivotKey
);
return $collection->unique()->all();
}
}
I came across the same problem quite some time ago and have thus been following this problem closely and have made a lot of research. I have come across some of the solutions you have also found, and some more, and also have thought of other solutions that I summed here, mostly how to get both user_ids in the same column. I am afraid they will all not work well. I am also afraid that using any custom classes will stop you from using all of Laravel's handy relation features (especially eager loading). So I still thought what one could do, and, until one comes up with a hasMany-function on many columns, I think I have come up with a possible solution yesterday. I will show it first and then apply it to your project.
My project
Initial solution
In my project, one user partners with another one (= partnership) and then later will be assigned a commission. So I had the following tables:
USERS
id | name
---------|------
1 | foo
2 | bar
17 | baz
20 | Joe
48 | Jane
51 | Jim
PARTNERSHIPS
id | partner1 | partner2 | confirmed | other_columns
----|-----------|-----------|-----------|---------------
1 | 1 | 2 | 1 |
9 | 17 | 20 | 1 |
23 | 48 | 51 | 1 |
As each user should always have only one active partnership, the non-active being soft-deleted, I could have helped myself by just using the hasMany function twice:
//user.php
public function partnerships()
{
$r = $this->hasMany(Partnership::class, 'partner1');
if(! $r->count() ){
$r = $this->hasMany(Partnership::class, 'partner2');
}
return $r;
}
But if I had wanted to lookup all partnerships of a user, current and past, this of course, wouldn't have worked.
New solution
Yesterday, I came up with the solution, that is close to yours, of using a pivot table but with a little difference of using another table:
USERS
(same as above)
PARTNERSHIP_USER
user_id | partnership_id
--------|----------------
1 | 1
2 | 1
17 | 9
20 | 9
48 | 23
51 | 23
PARTNERSHIPS
id | confirmed | other_columns
----|-----------|---------------
1 | 1 |
9 | 1 |
23 | 1 |
// user.php
public function partnerships(){
return $this->belongsToMany(Partnership::class);
}
public function getPartners(){
return $this->partnerships()->with(['users' => function ($query){
$query->where('user_id', '<>', $this->id);
}])->get();
}
public function getCurrentPartner(){
return $this->partnerships()->latest()->with(['users' => function ($query){
$query->where('user_id', '<>', $this->id);
}])->get();
}
// partnership.php
public function users(){
return $this->belongsToMany(User::class);
}
Of course, this comes with the drawback that you always have to create and maintain two entrances in the pivot table but I think this occasional extra load for the database -- how often will this be altered anyway? -- is preferable to having two select queries on two columns every time (and from your example it seemed that you duplicated the entries in your friends table anyway).
Applied to your project
In your example the tables could be structured like this:
USERS
id | name
---------|------
1 | foo
2 | bar
3 | baz
FRIENDSHIP_USER
user_id | friendship_id
---------|------
1 | 1
2 | 1
3 | 2
1 | 2
FRIENDSHIPS
id |send_id* | receive_id* | is_blocked | [all the other nice stuff
--------|---------|-------------|------------|- you want to save]
1 | 1 | 2 | 0 |
2 | 3 | 1 | 0 |
[*send_id and receive_id are optional except
you really want to save who did what]
Edit: My $user->partners() looks like this:
// user.php
// PARTNERSHIPS
public function partnerships(){
// 'failed' is a custom fields in the pivot table, like the 'is_blocked' in your example
return $this->belongsToMany(Partnership::class)
->withPivot('failed');
}
// PARTNERS
public function partners(){
// this query goes forth to partnerships and then back to users.
// The subquery excludes the id of the querying user when going back
// (when I ask for "partners", I want only the second person to be returned)
return $this->partnerships()
->with(['users' => function ($query){
$query->where('user_id', '<>', $this->id);
}]);
}
Is there any method available in Laravel 5/5.1, through which we can get the Table columns name, its type and length, Means table meta data?
eg:
Name | Type | Length
ID | Integer | 11
Name | varchar | 100
Email | varchar | 100
Password| md5 | 82
Address | tinytext|
DOB | date |
Status | enum(0,1)|
I attempted this but kept getting PDO errors because not all drivers are supported. So if you use MySQL or MariaDB this may help.
$columns = DB::select( DB::raw('SHOW COLUMNS FROM `'.$table.'`'));
foreach($columns as $column) {
$name = $column->Name;
$type = $column->Type;
}
You can check it here. https://laravel.com/api/5.1/Illuminate/Database/Connection.html
Sample code for getting type of password field from users table.
dd(DB::connection()->getDoctrineColumn('users', 'password')->getType()->getName());
I'll leave the rest to you. Goodluck :)
One thing I've added to my larger models at the beginning of projects is a handy static function which returns all table columns data as an array so it can be used with Tinker or otherwise:
public static function describe()
{
return DB::select(DB::raw("DESCRIBE " . (new self)->getTable(). ";"));
}
Then call this in tinker or otherwise with App\Models\ModelName::describe()
can anybody help me on the following query.
I have a table that holds a postcode column and a rating column, ie.
ID | POSTCODE | RATING
1 | sk101dd | E
2 | sk101de | A
3 | sk101df | E
4 | sk101dg | E
5 | sk101dh | D
etc
This is set up as a model called PostcodeList
I have a relational table, linked via the RATING column that holds a customer id and cost, ie.
ID | CUSTOMER_ID | RATING | COST
1 | 1234 | E | 0.05
2 | 9999 | E | 0.02
This is set up as a model called RatingCost. I linked this to PostcodeList model using the following code:
public function costing()
{
return $this->hasMany('PostcodeList','RATING','RATING');
}
I need to return the COST from the RatingCost model using CUSTOMER_ID as the filter without resorting to multiple sql statements. I think I've nearly got it, using the code below, but it's not quite right:
$p = PostcodeList::where('postcode',$somepostcode)->first();
$cost = $p->costing()->where('customer_id',$somecustomerid)->first()->cost;
The error I'm getting at the moment is "Trying to get property of non-object".
Any help greatly appreciated. I don't want to resort to DBRAW or another form of join as I really like the relational setup Laravel provides.
thanks
I know you're trying to stay away from joins, but this Laravel query would produce the desired results:
DB::table('PostcodeList')
->join('RatingCost', function($join)
{
$join->on('RATING', '=', 'RatingCost.RATING')
->->where('customer_id',$somecustomerid)
})
You have this
$postcode_get = PostcodeList::where('postcode',$somepostcode)->get();
foreach($postcode_get as $p){
...
$cost = $p->costing()->where('customer_id',$somecustomerid)
// ...
}
You have defined the method costing in your RatingCost model but calling it from PostcodeList model, in this case you need to declare the relation inside your PostcodeList model like this:
public function costing()
{
// change the field name 'RATING' in any table, maybe
// prefix with table name or something else, keep different
return $this->belongsToMany('RatingCost','RATING', 'RATING');
}
So, you can use this (inside loop):
$cost = $p->costing();
Because, inside your loop each $p represents a PostcodeList model and $postcode_get is a collection of PostcodeList models.
I don't think what I'm trying to do is actually possible without using joins. So the solution was to scrap the belongsTo and hasMany options for a standard join (similar to dev_feed's response):
$pr = PostcodeList::join('RatingCost', function($join)
{
$join->on('PostcodeList.content_rate', '=', 'RatingCost.code');
})
->where('postcode', '=', $somepostcode)
->where('RatingCost.customer_id','=',$somecustomerid)
->first();
I want to show the names of all our project leaders in a dropdown list.
The project leaders are only some of the employees that work in the company.
Here are my tables:
project_leaders
,----,----------------,
| id | hr_employee_id |
|----|----------------|
| 1 | 18 |
'----'----------------'
projects
,----,------,-------------------,
| id | name | project_leader_id |
|----|------|-------------------|
| 1 | cake | 1 |
'----'------'-------------------'
hr_employees
,----,------,---------,
| id | name | surname |
|----|------|---------|
| 18 | joe | dirt |
'----'------'---------'
My ProjectsController looks like this:
public function add() {
if ($this->request->is('post')) {
$this->Project->create();
if ($this->Project->save($this->request->data)) {
$this->_sendProjectRequestEmail($this->Project->getLastInsertID() );
$this->Session->setFlash(__('The project has been saved'));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(__('The project could not be saved. Please, try again.'));
}
}
$this->set('projectLeaders',$this->Project->ProjectLeader->find('list');
}
This only returns the id of the Project Leaders, not the name and surname. So instead of Joe Dirt, it returns 1.
I've tried doing $this->Project->ProjectLeader->HrEmployee->find('list') but that lists all the employees.
I've also tried specifying the fields, but it returns a unknown field error.
What am I doing wrong?
$this->set('projectLeaders',$this->Project->ProjectLeader->find('list'));
This will just list the records from the project_leaders table and most likely, as the table doesn't itself contain a name/title field (which cake would pick up automatically as the displayField) be like so:
array(
1 => 1
)
Use an appropriate find
To get a meaningful list of project leaders, you need to ensure you get a join between project_leaders and hr_employees one way to do that is using the containable behavior and simply specifying which fields to use in the list:
$projectLeaders = $this->Project->ProjectLeader->find('list', array(
'contain' => array(
'HrEmployee'
),
'fields' => array(
'ProjectLeader.id',
'HrEmployee.name',
)
));
$this->set(compact('projectLeaders'));
Is your db structure appropriate for your needs?
Having a table equivalent to saying "this user is admin" may not be the best idea - it would be easier to avoid data problems, and give you simpler queries, if the table project_leaders was (only) a boolean field on your hr_employees table and project_leader_id pointed at the hr_employee table, not some other data-abstraction.
Of course I don't know your whole schema, there very well may be a good reason for having a seperate project_leaders table.
Alternatively - denormalize
If you add a name field to project_leaders - you don't need a join to know the name of a project leader or any funkiness:
alter table project_leaders add name varchar(255) after id;
update project_leaders set name = (select name from hr_employees where hr_employees.id = hr_employee_id);
In this way you can know who is the relevant project lead with one query/join instead of needing to do two joins to get an employee's name.
You forgot to define the displayField:
The default case is
public $displayField = 'name';
And only for an existing "name"/"title" field in this table cake will automatically know what your drop-downs should get as label text.
To use the fields of other tables, make sure you pass fields in your find() options:
'fields' => array('Project.id', 'Project.name');
etc. Cake will then know: the first one is the key, and the second one the display value.
If you need combined/custom fields/texts in your dropdowns use virtualFields
public $virtualFields = array(
'full_name' => 'CONCAT(...)'
);
and set the displayField to this full_name virtual field then or use fields as explained above.
You could add a virtual field to your ProjectLeader Model:
public $virtualFields = array (
'name' => 'SELECT name FROM hr_employees AS Employee WHERE Employee.id = ProjectLeader.employee_id'
);
Also, add this virtual field as your displayField:
public $displayField = 'name';