Cakephp 3: Set virtual property with condition on belongsToMany association - php

I have two tables, Tickets and Paintings. A ticket can have many paintings and a painting can be used on many tickets. They have a join table called tickets_paintings with ticket_id and painting_id. Here's how the tables are set:
class TicketsTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Paintings', [
'foreignKey' => 'ticket_id',
'targetForeignKey' => 'painting_id',
'joinTable' => 'tickets_paintings'
]);
}
class PaintingsTable extends Table
{
public function initialize(array $config)
{
$this->belongsToMany('Tickets', [
'foreignKey' => 'painting_id',
'targetForeignKey' => 'ticket_id',
'joinTable' => 'tickets_paintings'
]);
}
Every Ticket has a field "active" that is boolean and tells me if the ticket is currently in use or not. In the edit function the field can be changed to true or false.
But now I also need a property for every Painting, that tells me if this painting is currently used on an active ticket. In this case it would be unavailable to be used on another ticket.
I thought I could add a boolean virtual property for a painting, that checks if it is (currently) associated with any ticket that has active => true and is therefore also set true or false.
Like: "Does this painting belong to a ticket with state active => 0".
And then set the virtual property accordingly, so I can display it in a view.
I managed to create a virtual property for a painting that checks if there is an entry for its ID inside the join table so far:
class Painting extends Entity
{
protected function _getIsAvailable(){
$TicketsTable = TableRegistry::get('TicketsPaintings');
$exists = $TicketsTable->exists(['painting_id' => $this->id]);
if($exists == true){
return 1;
} else {
return 0;
}
}
}
A) How could I add a condition that checks if any matching ticket_id in the join table has an active=> 1 in the original Tickets table?
The query would have to check the ticket_id in the join table in an DESC order and return true as soon as the first active ticket is found (so it does not check the whole thing every time)
I don't even understand how to access that "active" property on the tickets table to check it. I assume I would have to use the " has Many through" option?! But even trying to following the explanation in the book and this question exactly (for hours), I am not able to get this to work for my example because I don't understand the correct syntax I had to use for my tables.
B) Is it even possible or "advisable" to check sth like this inside the Entity?
I tried to use an extra db field in the paintings table first but it seemed much more complicated to check and set another value every time a ticket is edited (not that I managed to make that work either)...and the active value is pretty much doubled then for ticket and painting. So I thought the virtual property would be easier to handle. Or am I on the completely wrong train?
Thanks for any tips or advice on this nightmare =)!

Since nobody came to my rescue I managed to put something together and actually create the virtual property I wanted. I don't know if this is the best way to go but it works for me and I will put it here in case anyone is searching for similar case.
I created a finder method inside the Paintings Table that joins the 3 tables and filters out all paintings that are connected to an active ticket.
class PaintingsTable extends Table
{
public function findNotavailable(Query $query, array $options){
$query
->join([
'tickets_paintings' =>[
'table' => 'tickets_paintings',
'type' => 'LEFT',
'conditions' => 'tickets_paintings.painting_id = Paintings.id'
],
'tickets' => [
'table' => 'tickets',
'type' => 'LEFT',
'conditions' => 'tickets_paintings.ticket_id = Tickets.id'
],
])
->select(['Paintings.id'])
->order(['tickets_paintings.ticket_id' => 'DESC'])
->where([
'Tickets.active' => false
]);
return $query;
}
}
Then in the Entity of the Painting I check if its ID is inside this selection and set a boolean virtual property accordingly:
use Cake\ORM\TableRegistry;
class Painting extends Entity
{
protected function _getNotAvailable(){
$Paintings = TableRegistry::get('Paintings');
$notavailable = $Paintings->find('notavailable')
->where(['Paintings.id'=>$this->id]);
if($notavailable->isEmpty()){
// if no record --> painting is available
return 0;
} else {
// if record found --> painting is not available
return 1;
}
}
}
And in the view I display it like this:
In Paintings - view.ctp
<?= $painting->not_available ? __('not available') : __('available'); ?>
The only downside to it is that virtual properties can't be used for pagination as far as I understand the book. But I'm sure there's a workaround for that too.

Related

Problem with Polymorphic Relationships in Laravel

I'm trying to implement a way to get the details of a person depending on the group it belongs to.
My database looks like this:
persons:
id
group
type
1
person
9
2
company
30
3
person
9
and so on.
Each "group" has a model which contains detail information for this record specific to the group.
For example:
persondetails looks like this
id
person_id
firstname
lastname
birthname
1
1
Harry
Example
Bornas
2
3
Henrietta
Example
Bornas
I created models for each table and I'm no trying to implement a relationship which allows me to query a person->with('details') via the person model (for example: for a complete list of all persons no matter which type it is).
For single records I got it working via a simple "if $this->group === person {$this->hasOne()}" relation, which doesn't work for listings.
I tried to wrap my head around a way to use a polymorphic relationship, so I put the following into the person model:
public function details(){
Relation::morphMap([
'person' => 'App\Models\Persondetail',
'company' => 'App\Models\Companydetail',
]);
return $this->morphTo();
}
and a subsequent
public function person(){
return $this->morphMany(Person::class, 'details');
}
which doesn't work sadly. Where is my thinking error?
As you're not using laravel convention for the keys, you need to define the keys on your relation
public function details()
{
Relation::morphMap([
'person' => 'App\Models\Persondetail',
'company' => 'App\Models\Companydetail',
]);
return $this->morphTo(__FUNCTION__, 'group', 'type');
}
Docs Link:
https://laravel.com/docs/9.x/eloquent-relationships#morph-one-to-one-key-conventions
Based on the reply by https://stackoverflow.com/users/8158202/akhzar-javed I figure it out, but I had to change the code a bit:
Instead of the code in the answer, I had to use the following:
public function details()
{
Relation::morphMap([
'person' => 'App\Models\Persondetails',
'company' => 'App\Models\Companydetail',
]);
return $this->morphTo(__FUNCTION__, 'group', 'id', 'person_id');
}

Can't get backpack to pass correct value when inserting/updating a many to many relationship

I have created a many to many relationship between two tables with a third pivot table.
The thing that makes the situation a little difficult is I am linking the Apps table based on name and not ID. It is because I update the App list from a third party and app name will always be consistent, where ID can possibly change if App is removed at some point, and then re-added, etc.
Apps
id
name // This is the name of the app, it will never change for a particular app and is short, all lowercase, no spaces, and unique
label // This is the user friendly name
Plans
id
name
etc
apps_plans pivot table
id
apps_name
plans_id
I've finally got everything working perfectly in Laravel itself, but I cannot figure out at all how to get this to work correctly in Backpack for my Admin portal. I've gotten it to the point where everything works perfect until I try to update or create a new plan. The Apps I select using the select2 type, it tries to insert them into the pivot table with an ID number and not with the name.
Randomizing some names, my mistake if things don't match perfectly. This aspect works fine from all tests I've done:
Plans Model:
{
use CrudTrait;
protected $table = 'plans';
protected $guarded = ['id'];
public function apps()
{
return $this->belongsToMany('App\Apps', 'apps_plans', 'plans_id', 'apps_name', 'id', 'name');
}
}
Apps Model:
class Apps extends Model
{
use CrudTrait;
protected $table = 'apps';
protected $guarded = ['id'];
protected $casts = [
'json' => 'array',
];
public function plans()
{
return $this->belongsToMany('App\Plan', 'apps_plans', 'apps_name', 'plans_id', 'name', 'id');
}
}
**Note I removed the fillable variable , I didn't want to expose all variables in my columns.
Backpack Plans CrudController:
public function setup()
{
CRUD::setModel(\App\Plan::class);
CRUD::setRoute(config('backpack.base.route_prefix') . '/plan');
CRUD::setEntityNameStrings('plan', 'plans');
$this->crud->addColumn([
'name' => 'apps',
'type' => 'relationship',
'label' => 'Apps',
'entity' => 'apps',
'attribute' => 'label',
'model' => \App\Apps::class,
]);
}
protected function setupCreateOperation()
{
CRUD::setValidation(PlanRequest::class);
CRUD::setFromDb(); // fields
$this->crud->addField('apps', [
'name' => 'apps',
'type' => 'select2_multiple',
'entity' => 'apps',
'attribute' => 'label',
'label' => 'Apps',
'pivot' => true,
]);
I removed quite a bit to keep my project details private, I hope it makes sense. I think all important details are still in. Anyone know if this is an issue with Backpack? Or did I miss an option somewhere, where you can set which column it uses for the relationship. It is clearly not taking it from the model because the models work just as intended on their own...
Thanks!
Edit: here is my migration I am using, it works flawlessly--even in phpmyadmin it gives me a drop down of items to select from
{
Schema::create('apps_plans', function (Blueprint $table) {
$table->id();
$table->string('apps_name');
$table->foreign('apps_name')->references('name')->on('apps');
$table->unsignedBigInteger('plans_id');
$table->foreign('plans_id')->references('id')->on('plans');
});
}
EDIT 2:
This is the error I am getting when trying to do a Create or Update:
{
"error": "There is a problem with your request",
"message": "SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`api`.`apps_plans`, CONSTRAINT `apps_plans_apps_name_foreign` FOREIGN KEY (`apps_name`) REFERENCES `apps` (`name`)) (SQL: insert into `apps_plans` (`apps_name`, `plans_id`) values (2, 4))"
}
Again I removed some details that were very specific to my project but I don't think I changed any of the logic in the error message. You can see everything looks great about the query except that at the very end, it is inserting the App ID instead of the App name as it should be.
I suspect that the current configuration will have the same result in Laravel directly. ie running something like $plan = Plan::find($somePlanId); $app = App::find($someAppId); $plan->apps()->attach($app); would result in the same error.
Since name is the key that matters for the apps table, consider dropping the autoincrementing id for that table and instead setting
In the migration for the apps table, do:
$table->string('name')->primary();
Then in your apps model, do:
protected $primaryKey = 'name';
public $incrementing = false;
protected $keyType = 'string';
Now, Laravel (and by proxy Backpack) should treat the relationship the way you expect.

OctoberCMS filter Relation Widget based from currently selected record

The title says it all, but to give an example. I have a Member record and a Group. A member can have memberships in many groups and a group can have many members. (So that's many to many and I would have a pivot table for it.)
Now, each group has membership grades. E.g., (Free, Freemium, Premium, Super Premium). So the membership_grade shall belong to the pivot table, right? But here's the problem, not all groups share the same grades. Some might have Free and Freemium only, some might have all.
In the fields.yaml of the Membership pivot model, I defined the membership_grades as a Relation Widget, like this:
pivot[grade]:
label: Membership Grade
span: full
type: relation
nameFrom: name
And in its relationship in Membership.php like this:
public $belongsTo = [
'grade' => [
'Acme\Models\Grade',
]
];
Obviously, this will expose ALL grades, since I'm pulling data from the Grade model. What I want is to expose the grades that is just available on that group, not all.
What I've thought to do (but I didn't, because it seemed impossible) is to try to pull data from the grades relationship of the Group, but how am I suppose to do that? (Since Relation widget manages the relation of the Model, I cannot simply pull data from other sources just like that).
Also I've tried to do scopes but how am I suppose to pass the current Group I'm in? Since it is needed as the filter, like this:
// Membership.php
public $belongsTo = [
'grade' => [
'Acme\Models\Grade',
'scope' => 'filteredIt'
],
// added this relationship to try the scopes approach
'group' => [
'Acme\Models\Group'
]
];
// Grade.php
public function scopeFilteredIt($query, Membership $m)
// yes, the second parameter in the scope will be the
// current Membership model. I've tried it.
{
// this won't work, since we want the overall relation filter;
// an instance of Membership won't help.
// this would work if I can find a way to pass the
// current Group (record) selected, and get its grades, then use it here.
return $query->whereIn('id', $m->group->grades->pluck('id')->all());
}
Any thoughts?
I have noticed some post values during pivot model ajax call.
When you add new record and when your pivot model opens post values are like this
Array (
[_relation_field] => groups
[_relation_extra_config] => W10=
[foreign_id] => 1
[_session_key] => VrSCoKQrSkIsZNGIju5QIqpdbS3AADoGQRHAsv1e
)
So good thing is that we can now get foreign_id as it will be your selected group id
and we can use it at creation time and for update time you know we have relation so we use that.
public function scopefilteredIt($query, Membership $m)
{
// we are checking relation is there or not
if($m->group) {
// yes group is there we use it
return $query->whereIn('id', $m->group->grades->pluck('id')->all());
}
else {
// seems new record then use foreign_id
$foreign_id = post('foreign_id'); //<-this will be your selected group id
if($foreign_id) { // <- double check if its there
$group = Group::find($foreign_id);
return $query->whereIn('id', $group->grades->pluck('id')->all());
}
}
return $query;
}
please comment if you get any issue.
to check post
public function scopefilteredIt($query, Membership $m)
{
// will show flash message with post data array
$post = print_r(post(), true);
\Flash::success($post);
// we are checking relation is there or not
if($m->group) {
// yes group is there we use it
return $query->whereIn('id', $m->group->grades->pluck('id')->all());
}
else {
// seems new record then use foreign_id
$foreign_id = post('foreign_id'); //<-this will be your selected group id
if($foreign_id) { // <- double check if its there
$group = Group::find($foreign_id);
return $query->whereIn('id', $group->grades->pluck('id')->all());
}
}
return $query;
}

How to filter associated HABTM data with FriendsOfCake Search for CakePHP 3

Plugin: FriendsOfCake/Search
CakePHP: 3.1.4
I'm using the plugin to filter my index.ctp view data with a form.
This similar question:
How to Filter on Associated Data
is about a belongsTo association. My question is specifically about associated HABTM data where my associated table is linked through a joinTable and not directly. The normal setup in the Model like the following is not working in this case:
->value('painting', [
field' => $this->Paintings->target()->aliasField('id')
)]
My tables are set up like:
Tickets belongsToMany Paintings
Paintings belongsToMany Tickets
with joinTable tickets_paintings
Here is the main setup:
class TicketsTable extends Table
{
public function initialize(array $config)
{
...
$this->belongsToMany('Paintings', [
'foreignKey' => 'ticket_id',
'targetForeignKey' => 'painting_id',
'joinTable' => 'tickets_paintings'
]);
}
public function searchConfiguration()
{
$search = new Manager($this);
$search->value('status', [
'field' => $this->aliasField('active'),
])->like('member_name', [
'field' => $this->Members->target()->aliasField('surname'),
'filterEmpty' => true
])->value('painting', [
'field' => $this->Paintings->target()->aliasField('id'), // not working
]);
return $search;
}
class TicketsController extends AppController
{
public function index()
{
$query = $this->Tickets
->find('search',
$this->Tickets->filterParams($this->request->query))
->contain(['Members', 'Paintings', 'Appointments']);
...
}
Everything else is working and the parameters are added to the URL when I filter etc., so I only put in the parts where sth has to be wrong.
After filtering I get an error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Paintings.id' in 'where clause'
The contain works properly when just displaying data from the Paintings table in the Tickets view.
But in the code from the SQL query I can see, that all contained tables (Members, Appoinments) are joined for the query except the Paintings table, so obviously it can not find the column...And I guess it can't really join it directly anyway since they are only connected through the joinTable.
I'm new to CakePHP and I can't really figure out what I'm doing wrong here, so hopefully someone can help me out a bit.
Do I have to use a different syntax in the plugin settings? Do I have to set up my Tables differently? Or how exactly can I tell the query to incorporate the habtm related table in the search?
Thanks!
The available search methods rely on the field being available in the main query (hasMany and belongsToMany associations are being being retrieved in separate queries).
While you could join it in manually in the controller, using a callback- or a finder-filter is probably the better approach, that way you can modify the query in the model layer, and you could easily utilize Query::matching() to filter by associated data.
Here's an (untested) example that should give you a hint:
use Cake\ORM\Query;
use Search\Type\Callback; // This changed in master recently
// now it's Search\Model\Filter\Callback
// ...
public function searchConfiguration()
{
$search = new Manager($this);
$search
// ...
->callback('painting', [
'callback' => function (Query $query, array $args, Callback $type) {
return $query
->distinct($this->aliasField('id'))
->matching('Paintings', function (Query $query) use ($args) {
return $query
->where([
$this->Paintings->target()->aliasField('id') => $args['painting']
]);
});
}
]);
return $search;
}
See also
https://github.com/FriendsOfCake/search/blob/9e12117404f824847b2d1aa093f3d52736b658b4/README.md#types
https://github.com/FriendsOfCake/search/blob/master/README.md#filters
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Filtering by Associated Data

Kohana 3 ORM Relationships Question

I've been through several sites (including this one), and unfortunately as a Kohana newbie I still can't get this to work. The data relationship is fairly simple, I have a company record, which should be linked to 1 status record and 1 type record. Of course there will be multiple companies in the table, but each company is only allowed to be linked to 1 of each (and must be).
What I have is:
class Model_Company extends ORM
{
protected $_has_one = array(
'companystatus' => array('model' => 'companystatus', 'foreign_key' => 'entryid'),
'companytype' => array('model' => 'companytype', 'foreign_key' => 'entryid')
,
);
}
Company Status Model:
<?php defined('SYSPATH') or die('No direct access allowed.');
class Model_CompanyStatus extends ORM
{
protected $_table_name = 'datadictionary';
protected $_primary_key = 'entryid';
protected $_has_many = array(
'company' => array('foreign_key' => 'statusid')
,
);
}
?>
Company Type Model:
<?php defined('SYSPATH') or die('No direct access allowed.');
class Model_CompanyType extends ORM
{
protected $_table_name = 'datadictionary';
protected $_primary_key = 'entryid';
protected $_has_many = array(
'company' => array('foreign_key' => 'companytypeid')
,
);
}
?>
The companystatus and companytype models are mapped to a single table which has 2 fields, entryid and entryname. This table is called "datadictionary", and has the appropriate properties so that I don't have to use "id" as the record id field.
Now I load my Company record like this:
$company = ORM::factory('company')
->where('id', '=', 1)
->where('hasbeendeleted', '=', 0)
->find();
The problem is that I don't get anything back for the companystatus and companytype properties for the company, and when I do a $company->companystatus->find() I get the first record returned, which is weird. What am I missing?
Thanks!!
:-)
Edit:
For simplicity's sake the Companies table has the following fields:
ID (primary key) - auto inc int
CompanyName - varchar(255)
StatusID - int
CompanyTypeID - int
HasBeenDeleted - smallint (0 for false, 1 for true)
DataDictionary Table:
EntryID (primary key) - auto inc int
EntryName - nvarchar(255)
Example Company record:
ID: 1
CompanyName: TestCompany
StatusID: 1
CompanyTypeID: 3
HasBeenDeleted: 0
Example DataDictionary records:
EntryID: 1
EntryName: Active
EntryID: 2
EntryName: Inactive
EntryID: 3
EntryName: Customer
EntryID: 4
EntryName: Supplier
There are a few things here I would try changing.
First of all, for readability, most people use underscores in foreign keys. So instead of entryid, I'd recommend using entry_id (you'd have to make the change in both your database and your code).
In Kohana 3, declaring 'model' => 'companystatus' in a $has_one array is redundant when the key is the same as the model name. You can safely remove that part.
But really, that's all incidental to your problem, which exists somewhere between that last ORM call and your database. (I'm assuming here that hasbeendeleted is a column in the company table, not either of the other two tables you mentioned. Let me know if that's not the case.)
If you're doing a ->where('id', '=', 1) together with a ->find(), you're really expecting to return the one company record if it exists in the database. I would recommend making a separate check for hasbeendeleted.
And speaking of which, instead of naming that variable $companies, it should really be singular (e.g. $company) since it will only hold one record.
And you can simplify ORM::factory('company')->where('id', '=', 1) to simply ORM::factory('company', 1)
If you know for sure that a company with a database ID of 1 exists, then the following code should return that record:
$myCompany = ORM::factory('company', 1);
Then you can do something like if ( ! $myCompany->hasbeendeleted) ...
That should help you a bit. Post more details if you run into trouble.

Categories