Cakephp 3.9 Multiple Associations with the Same Table - cakephp-3.x

I am working on Cakephp 3.9 and I have 3 tables in my application.
CREATE TABLE `leads` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`first_name` varchar(100) NOT NULL,
`last_name` varchar(100) NOT NULL,
`created_dt` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1
CREATE TABLE `clients` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`lead_id` int(10) NOT NULL,
`first_name` varchar(100) NOT NULL,
`last_name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1
CREATE TABLE `contacts` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`lead_id` int(10) NOT NULL,
`first_name` varchar(100) NOT NULL,
`last_name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1
There is a one-to-one relation between the Leads, Clients, and Contacts tables.
I also have a Users table:
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`created` datetime DEFAULT NULL,
`modified` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=latin1
... and a leads_users table
CREATE TABLE `leads_users` (
`lead_id` int(10) NOT NULL,
`user_id` int(10) NOT NULL,
`user_type` enum('Opener','Closer') NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin
In my application, the users can be associated with the leads as Openers and Closers. There can be multiple openers and multiple closers for a lead. I have defined my models as below:
class LeadsTable extends Table
{
public function initialize(array $config)
{
$this->hasOne('Contacts');
$this->hasOne('Clients');
$this->belongsToMany('Users', [
'className' => 'Users'
])
->setConditions(['LeadsUsers.user_type' => 'Opener'])
->setProperty('openers');
$this->belongsToMany('Users', [
'className' => 'Users'
])
->setConditions(['LeadsUsers.user_type' => 'Closer'])
->setProperty('closers');
}
}
class ClientsTable extends Table
{
public function initialize(array $config)
{
$this->belongsTo('Leads');
}
}
class ContactsTable extends Table
{
public function initialize(array $config)
{
$this->belongsTo('Leads');
}
}
class UsersTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Leads');
}
}
Now in the Leads controller, I am trying to find all the leads, their contacts, clients, openers, and closers by using the below line of code.
$result = $this->Leads->find('all')->contain(['Contacts', 'Clients', 'Users']);
With this, I am getting the closers but not the openers. In the LeadsTable, if I comment out the closers association then I am getting the openers. So looks like the second association is overwriting the first one.
If I change the LeadsTable association to below:
class LeadsTable extends Table
{
public function initialize(array $config)
{
$this->hasOne('Contacts');
$this->hasOne('Clients');
$this->belongsToMany('Openers', [
'className' => 'Users'
])
->setConditions(['LeadsUsers.user_type' => 'Opener'])
->setProperty('openers');
$this->belongsToMany('Closers', [
'className' => 'Users'
])
->setConditions(['LeadsUsers.user_type' => 'Closer'])
->setProperty('closers');
}
}
I am getting the below error:
The Users association is not defined on Leads.
Please let me know what am I doing wrong.
Thank you in advance.

You're getting this error because you have to change your controller query as well.
$result = $this->Leads->find('all')->contain(['Contacts', 'Clients', 'Closers','Openers']);

So I ended up changing my database a bit and that fixed it.
CREATE TABLE `leads_openers` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`lead_id` int(10) NOT NULL,
`opener_id` int(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1
CREATE TABLE `leads_closers` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`lead_id` int(10) NOT NULL,
`closer_id` int(10) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1
and then I changed my models like below:
<?php
namespace App\Model\Table;
use Cake\ORM\Table;
class OpenersTable extends Table
{
public function initialize(array $config)
{
$this->setTable('users');
$this->setPrimaryKey('id');
$this->belongsToMany('Leads', [
'joinTable' => 'leads_openers',
]);
}
}
<?php
namespace App\Model\Table;
use Cake\ORM\Table;
class ClosersTable extends Table
{
public function initialize(array $config)
{
$this->setTable('users');
$this->setPrimaryKey('id');
$this->belongsToMany('Leads', [
'joinTable' => 'leads_closers',
]);
}
}
<?php
namespace App\Model\Table;
use Cake\ORM\Table;
class LeadsTable extends Table
{
public function initialize(array $config)
{
$this->addBehavior('Timestamp');
$this->belongsToMany('Openers', [
'joinTable' => 'leads_openers',
])
->setProperty('openers');
$this->belongsToMany('Closers', [
'joinTable' => 'leads_closers',
])
->setProperty('closers');
}
}
and then in the controller, I am doing this:
$leads = $this->Leads->find('all')
->where(['Leads.status IN' => $search_condition_lead])
->contain(['Contacts', 'Openers', 'Closers']);

Related

Laravel Eloquent: belongsToMany collection get value

I have following relation
Tasks has many to many relation with Categories
Categories has one to many relation with Types
I'm using this in my task controller currently to get all the data I want, which works almost as wanted:
$type = Type::with('categories.tasks')
->where('slug', $type)
->first();
This returns type, with a categories collection, each category has a tasks collection, and each tasks have a category collection. This structure is correct by design, problem is the category child collection to task returns all categories, but I want it to be limited to the same condition as the parent category collection which is where('slug', $type) and I want the collection return not to be an array since the result will always only return one category if condition is applied.
I've tried using scope but with no success. Any help would be much appreciated.
My table and respective models
Type Table
CREATE TABLE `types` (
`id` int(11) NOT NULL,
`slug` varchar(60) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp()
)
Type Model
class Type extends Model{
public function categories()
{
return $this->morphMany(Category::class, 'categorize');
}
public function project()
{
return $this->belongsTo(Project::class);
}
}
Category Table
CREATE TABLE `categories` (
`id` int(10) NOT NULL,
`title` varchar(128) NOT NULL,
`color` varchar(10) NOT NULL,
`content` varchar(128) NOT NULL,
`categorize_id` int(6) NOT NULL,
`categorize_type` varchar(256) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp()
)
Category Model:
class Category extends Model
{
public function categorize(){
return $this->morphTo();
}
public function tasks(){
return $this->belongsToMany(Task::class,'task_category');
}
public function types(){
return $this->belongsTo(Type::class);
}
}
Tasks Table
CREATE TABLE `tasks` (
`id` int(10) UNSIGNED NOT NULL,
`project_id` int(11) NOT NULL,
`title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`desc` text COLLATE utf8mb4_unicode_ci NOT NULL,
`is_completed` tinyint(1) NOT NULL DEFAULT 0,
`start` timestamp NOT NULL DEFAULT current_timestamp(),
`end` timestamp NULL DEFAULT NULL,
`allDay` tinyint(1) NOT NULL DEFAULT 0,
`created_at` timestamp NULL DEFAULT current_timestamp(),
`updated_at` timestamp NULL DEFAULT current_timestamp(),
`deleted_at` timestamp NULL DEFAULT NULL
)
Pivot table
CREATE TABLE `task_category` (
`task_id` int(11) NOT NULL,
`category_id` int(11) NOT NULL
)
Task Model
class Task extends Model
{
public function category(){
return $this->belongsToMany(Category::class,'task_category','task_id','category_id');
}
}
Edit: I have rephrased my question and added all information about models and tables
When using the data from
$type = Type::with('categories.tasks')
->where('slug', $type)
->first();
if you get the category from the task like this
foreach($type->categories as $category) {
foreach($category->tasks as $task) {
$wantedCategory = $task->categories; //like this
}
}
then yes, it will get you all the categories linked to that task,
what you need is just use the variable $category from your loop (or index)
we could help you more if you provide database structure and how you try to recover the category in question.

PHP function stripos() expects parameter 1 to be string, object given

I'm running through Laravel 6.0's addSelect() method as the official documentation described. However, I'm getting an error as my title suggested.
Two corresponding tables are:
CREATE TABLE `destinations` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(90) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1
CREATE TABLE `flights` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(90) NOT NULL,
`destination_id` int(11) NOT NULL,
`arrived_at` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=latin1
The corresponding controller:
use App\Flight;
use App\Destination;
class TestController extends Controller
{
public function Index()
{
return Destination::addSelect(['last_flight' =>
Flight::select('name')
->whereColumn('destination_id', 'destinations.id')
->orderBy('arrived_at', 'desc')
->limit(1)
])->get();
}
}
What is the object provided as a parameter, which is responsible for the error?

Access a column from another table in laravel eloquent

I have 2 tables as Page and Country in many to many relationship. I have to display a page according to slug and locale passed in my route. The locale field is in Country table. How can i access it in PageController
Here are my Schemas for both tables
CREATE TABLE `countries` ( `id` int(11) NOT NULL, `cname` varchar(255) NOT NULL, `lname` varchar(255) NOT NULL, `locale` varchar(255) NOT NULL, `status` tinyint(1) DEFAULT NULL, `created_at` datetime DEFAULT NULL, `updated_at` varchar(45) DEFAULT NULL, `slug` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) )
CREATE TABLE `pages` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `description` varchar(255) NOT NULL, `slug` varchar(255) DEFAULT NULL, `created_at` datetime DEFAULT NULL, `updated_at` datetime DEFAULT NULL, `status` tinyint(1) DEFAULT NULL, PRIMARY KEY (`id`) )
Here is the method i am using
public function findBySlugAndLocale($slug,$locale)
{
return $this->model->where('slug', str_slug(trim($slug)))->where('countries',str_slug(trim($locale)))->first();
}
In above method the slug and locale are coming from route so here is my route
Route::get('/{slug}/{locale}', ['as' => 'show','uses' => 'HomeController#findBySlugAndLocale']);
I want a query something like this
select * from `pages` where `slug` = home and `countries` = en
Here are my Models:
class Country extends Basemodel {
protected $table = "countries";
public function pages() {
return $this->belongsToMany('App\Models\Page', 'country_page', 'country_id', 'page_id')->withTimestamps();
}
}
class Page extends Basemodel {
protected $table = "pages";
public function countries() {
return $this->belongsToMany('App\Models\Country', 'country_page', 'page_id', 'country_id')->withTimestamps();
}
}
Well I solved the above using join... Here is the query... It may help someone with the same issue
public function findBySlugAndLocale($slug,$locale)
{
$results = $this->model
->join('country_page', 'pages.id', '=', 'country_page.page_id')
->join('countries', 'countries.id', '=', 'country_page.country_id')
->where('pages.slug', '=', $slug)
->where('countries.locale', '=', $locale)
->first();
return $results;
}
If I'm understanding your question correctly and you want to query the pages table based on a locale in the countries table you'll need to use whereHas:
return $this->model
->where('slug', str_slug(trim($slug)))
->whereHas('countries', function ($query) use ($locale) {
$query->where('locale', str_slug(trim($locale)));
})->first();
Hope this helps!

Laravel 5.4 Model Relationship

I have created three tables users, courses and user_courses as shown below
CREATE TABLE `users` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
`address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` enum('0','1') COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
CREATE TABLE `courses` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` text,
`description` text,
`price` varchar(255) DEFAULT NULL,
`schedule` varchar(255) DEFAULT NULL,
`duration` varchar(255) DEFAULT NULL,
`summary` text,
`skills` text,
`mode` enum('0','1') DEFAULT NULL COMMENT '0-Online 1 -Instructor',
`random_token` text,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1
CREATE TABLE `user_courses` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL,
`course_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1
Now with these tables, I want to bind relationship such as when I fetch Users I'm able to get courses for a user and when I get to courses I want users associated with the course.
Please help how I can achieve it.
I have got my answer so posting it here if it can help anyone.
The main thing here is to assign a many-to-many relationship. In my user model I have defined
public function courses()
{
return $this->belongsToMany('App\Course');
}
In course model
public function users()
{
return $this->belongsToMany('App\User');
}
Actually, it depends on how you want to use the relationship.In some parts of the code you will need $user->courses or more likely to query $course->users or both.
Now here user_course table will be assumed as a pivot table. So in model, you can write it as
public function courses()
{
return $this->belongsToMany('App\Course', 'user_courses');
}
Here you can also specify the actual names of the fields of that particular pivot table i.e user_courses table.Then, what all we have to do is just add two more parameters first is the current model field and then add the field of the model being joined like
public function courses()
{
return $this->belongsToMany('App\Course', 'user_courses','user_id', 'course_id');
}
So using the above model relationship you can easily fetch users with all the respective courses by
User::->with('courses')->get();
First Fix
First, fix the user_course table structure the users table has id as integer while it's being referenced in user_course in user_id as bigint.
Solution
The first thing to do is to create models in the application. Then create relationships within models and finally use those relationships.
Create models
By using command line
php artisan make:model Course
php artisan make:model UserCourse
You can create them manually if you like. By default, they will be created in app folder with namespace App. For example, the user model will be App/User and so on.
The user model will already exists as its shipped with laravel default installation.
Create relationships
In user model add the following function
public function courses()
{
$this->belongsToMany(Course::class, 'user_course');
}
You can leave the Course Model empty if you are not planning to make a reverese relation from course to user. The one above defines relation from user to courses
Usage
Say in a controller you can use this as
public function someFunctionInController()
{
$usersWithCourses = \App\User::with('courses')->get()->toArray();
//result of a single user record will look something like this
/**
[
'id' => 1,
'name' => 'some name'
... //other users table columns
//the courses will be an array of array content and will be automatically injected
'courses' => [[
'id' => 1 //the course id
... //course table columns,
'pivot' => [
'user_id' => 1,
'course_id' => 1
]
],[
'id' => 3 //the course id
... //course table columns,
'pivot' => [
'user_id' => 1,
'course_id' => 3
]
]]
]
**/
}

How to save INT fields as null in laravel

I'm getting an error when saving some empty text, textarea fields. Laravel forms this sql query:
SQLSTATE[HY000]: General error: 1366 Incorrect integer value: '' for column 'inside_area' at row 1 (SQL: insert into `ads` (`street`, `quarter`, `building_number`, `inside_area`, `price`, `admin_comment`, `valid_to`, `price_heating`, `rooms`, `floor_number`, `floors_number`, `kitchen_area`, `years`, `public_comment`, `video_url`, `3d_url`, `user_id`, `updated_at`, `created_at`) values (, , , , , , , , , , , , , , , , 1, 2017-03-13 14:33:50, 2017-03-13 14:33:50))
P.S. db table was created not in laravel way - I'm using existing tables, this may be important.
UPDATED: problem are only with INT fields, if they has empty form field on saving!
Table:
CREATE TABLE `ads` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`ad_type_id` int(11) DEFAULT NULL,
`city_id` int(11) DEFAULT NULL,
`street` varchar(255) DEFAULT NULL,
`quarter` varchar(255) DEFAULT NULL,
`building_number` varchar(255) DEFAULT NULL,
`inside_area` int(11) DEFAULT NULL,
`price` int(11) DEFAULT NULL,
`show_price_per_meter` int(1) DEFAULT NULL,
`price_heating` int(10) DEFAULT NULL,
`admin_comment` text,
`valid_to` datetime DEFAULT NULL,
`rooms` int(11) DEFAULT NULL,
`floor_number` int(11) DEFAULT NULL,
`floors_number` int(11) DEFAULT NULL,
`kitchen_area` int(11) DEFAULT NULL,
`balcony` int(1) DEFAULT NULL,
`balcony_glazed` int(1) DEFAULT NULL,
`years` int(11) DEFAULT NULL,
`dyn_house_type_id` int(11) DEFAULT NULL,
`dyn_heating_id` int(11) DEFAULT NULL,
`dyn_installation_ids` int(11) DEFAULT NULL,
`public_comment` text,
`video_url` varchar(11) DEFAULT NULL,
`3d_url` varchar(11) DEFAULT NULL,
`available_for_trade` int(1) DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
FORM:
{!! Form::open(['url'=>'ads']) !!}
{!! Form::number('inside_area', null, ['class'=>'form-control']) !!}
{!! Form::close() !!}
Route:
Route::resource('ads', 'AdsController');
Save action:
public function store() {
$input = Request::all();
\App\Ad::create($input);
return redirect('/ads/my_index');
}
P.S.2 If I provide any value to inside_area field, it goes ok and the next error is for price field.
You can use attribute mutator for each integer attribute. In these mutators you can prepare data before inserting into DB.
In your case you should create mutators in your model Ads for every field with the type INT:
// define a mutator for the field "inside_area"
public function setInsideAreaAttribute($value)
{
$this->attributes['inside_area'] = (int) $value;
}
I understand that creating such a mutator for each INT field is boring, but it will also grant you ability to control user input before inserting into DB.
You can find more information about mutators here (for the latest version of Laravel at this moment):
Laravel 5.4, Defining a Mutator:
https://laravel.com/docs/5.4/eloquent-mutators#defining-a-mutator
Perhaps, you are passing string data to 'inside_area' variable. Because there is more default null fields at your table, like 'city_id', 'ad_type_id'
another way is to use Iatstuti and provide in Ad.php model:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Iatstuti\Database\Support\NullableFields;
class Ad extends Model {
use NullableFields;
public $timestamps = true;
protected $nullable = [
'inside_area'
];
protected $fillable = [
'user_id',
'ad_type_id',
'inside_area'
];
}

Resources