I have 3 Models (Adresses(orders) / Users / Cities)
and I connect them via adresses_users(I know adresses is supposed to be with two d´s :/) and cities_users.
Now I have an action in Adresses Controller which should give me a list for all the cities available and I want a count how many users are in that city (by the cities_users connection.
//Get all cities
$cities = $this->Cities->find('list', [
'keyField' => 'id',
'valueField' => function ($cities) {
return $cities->get('full_info');
}
,'order' => ['name' => 'asc']])
->contain(['Users']);
I got so far, but I don´t know where I can use the count function for all the containing users. Anyone an idea or documentation link (https://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html#finding-key-value-pairs)
You can construct query like this,
$query = $this->Cities->find();
$query->select(['users_count' => $query->func()->count('Users.id'),
'city_name' => 'Cities.name']);
$query->leftJoinWith('CitiesUsers');
$query->group('Cities.id');
//debug($query->sql()); debug($query); //to check whether correct sql constructed
//debug($query->toArray());
This code is not tested, i wrote it to provide example.
Related
I have the following relationship:
Rounds hasMany Results
Results belongsTo Drivers
Results are shown on Rounds's view page: rounds/view/{id}. I want to sort Results based on Drivers.name.
This is my RoundsController::view method:
public function view($id = null)
{
$this->paginate = [
'sortableFields' => [
'Results.Drivers.name',
]
];
$round = $this->Rounds->get($id, [
'contain' => ['Championships' => ['Teams'], 'Tracks', 'Results' => ['Drivers', 'Constructors']],
]);
$this->set(compact('round'));
}
And in my rounds/view.ctp file, I have this:
<?= $this->Paginator->sort('Results.Drivers.name', __('Driver')); ?>
However, no matter what I change, it doesn't sort by driver name. Also, when I click it multiple times, the direction stays asc.
Why is it not working?
Like #ndm mentioned, your code is showing only one specific Round. View method/function is receiving an $id arg, this $id is related to the round id you want to show in view page.
You can not order this (you can do it but it doesnt make sense) because you are receiving only one Round, and it doesnt make sense to order only one row.
This view method is usually used with this goal: show individual/personal information about an Entity (Round entity in your case). Please, note I use the word USUALLY.
I think you could use the index method for this purpose (this method/function is usually used to list entities from a model and its association data). You could create a new method/function for your porpuse as well if you already are using index method for another goal.
I suggest something like this, assuming you will use the index function:
public function index()
{
$rounds = $this->Rounds->find('all')
->contain([
'Championships' => ['Teams'],
'Tracks',
'Results' => [
'Constructors'
[
'Drivers' => function (Query $q) {
return $q->order('Drivers.name');;
}
]
]
]);
$rounds= $this->paginate($rounds);
$this->set(compact('rounds'));
}
You could pass conditions into the contain or do the same into paginate method. I always use the first (just preferences).
NOTE: this code may not work, it is an idea of what you could do.
Finders documentation: https://book.cakephp.org/4/en/orm/retrieving-data-and-resultsets.html#using-finders-to-load-data
Passing conditions to contains: https://book.cakephp.org/4/en/orm/query-builder.html#passing-conditions-to-contain
I have an Auth process which works fine with one userModel. But not only because of my DB schema I need to have one login method/action which works with multiple models.
So far I've tried everything I was able to think of or find online - for example editing this Cake 1.3 solution into Cake 3 and a few more hints I was able to find.
However, I'm not able to figure it out.
Thank you for any answer.
My AppController component load:
$this->loadComponent('ExtendedAuth', [
'authenticate' => [
'Form' => [
//'userModel' => 'Admins',
'fields' => [
'username' => 'email',
'password' => 'password'
]
]
],
'loginAction' => [
'controller' => 'Admins',
'action' => 'login'
],
// If unauthorized, return them to page they were just on
'unauthorizedRedirect' => $this->referer(),
]);
My ExtendedAuthComponent:
class ExtendedAuthComponent extends AuthComponent
{
function identify($user = null, $conditions = null) {
$models = array('Admins', 'Users');
foreach ($models as $model) {
//$this->userModel = $model; // switch model
parent::setConfig('authenticate', [
AuthComponent::ALL => [
'userModel' => $model
]
]);
$result = parent::identify(); // let cake do its thing
if ($result) {
return $result; // login success
}
}
return null; // login failure
}
}
EDIT1: Description of situation
I have two separate tables (Admins, Users). I need just one login action which tries to use Admins table prior to Users. Because of the application logic I can't combine them to one table with something like 'is_admin' flag. So basically what I need is instead of one specific userModel set in Auth config, I need a set of models. Sounds simple and yet I'm not able to achieve it.
EDIT2: Chosen solution
Based on the answer below, I decided to update my schema. Auth users table is just simplified table with login credentials and role and other role-specific fields are then in separate tables which are used as a connection for other role-specific tables. Even though the answer is not exactly a solution for the asked question, it made me think more about any possible changes of the schema and I found this solution because of it so I'm marking it as a solution. I appreciate all comments as well.
As Mark already said in a comment: Don't use two users tables. Add a type field or role or whatever else and associated data in separate tables if it's different like admin_profiles and user_profiles.
Don't extend the Auth component. I wouldn't recommend to use it anymore any way because it's going to get deprecated in the upcoming 3.7 / 4.0 release. Use the new official authentication and authorization plugins instead.
If you insist on the rocky path and want to make your life harder, well go for it but then you should still not extend the auth component but instead write a custom authentication adapter. This is the right place to implement your custom 2-table-weirdness. Read this section of the manual on how to do it.
I have two tables: Ingredients and Customers. The relationship between them is that Customers hasMany Ingredients. By default when doing the cakebake using the console, the only way to change them is by assigning an ingredient to the customer in the Ingredients page. However, I want to have in Customers page a checkbox list of Ingredients that can be assigned. Is it possible to do this? If yes, how?
edit:
What I have done until now is that I add this code to my add.ctp:
echo $this->Form->input('Ingredient',
array('label'=>'',
'type'=>'select',
'multiple'=>'checkbox',
'options'=>$ingredients));
However, it gives me "Undefined variable: ingredients" error when I tried to open the add view.
You want and need a HABTM relationship. Different customers can access and use the same ingredients. Look at the docs here yours would be very similar to different Posts using same Tags.
If you are getting this error:
"Undefined variable: ingredients"
It sounds like you haven't declared this variable in your controller, and set it so that the view can use it. Without knowing your code, you would probably need do do something like this (please note I am guessing what your application structure looks like and have not tested this code).
CustomersController.php
// The controller action for your view
public function view() {
// Get the ID and name of all your ingredients
$ingredients = $this->Ingredient->find('all', array(
'fields' => array('id', 'name'),
'order' => 'name',
'recursive' => -1
));
// We will use this array to store all the HTML select options
$ingredientOptions = array();
// Loop through all the ingredients and add them to the select
// options in a format that is suitable for CakePHP to use in
// the view to build your HTML select menu.
foreach ($ingredients as $i) {
$ingredient = $i['Ingredient'];
$ingredientOptions[$ingredient['id']] = $ingredient['name'];
}
// Make the variable available to the view
$this->set('ingredients', $ingredientOptions);
}
I have two main tables:
Books (id, author, isbn, ...)
Users (id, username, password, ...)
I am looking at building an application, wherein User1 logs in and can:
view list of all books (eg. only title)
view details (author, isbn, ...) of only certain books that he should have access to
Each user may have access to a certain set of books. I don't need various roles.
I have setup the MVC and the relationships (habtm) for the above. I am now looking at developing the permissions. Do you think the CakePHP ACL solves this problem or is it overkill?
If it is overkill, is there another component or easier way to build the desired functionality?
Yes, ACL is overkill
ACL is a very powerful and flexible system - but it doesn't come free it brings with it complexity. Unless you have a usecase where you absolutely need fine-grained permissions (the two rules you've described do not fit this) - don't use ACL.
Restricting to books a user he has added
This rule is easy to implement - e.g. add to relevant find calls:
$results = $BookModelInstance->find('all', array(
'conditions' => array(
'created_by' => AuthComponent::user('id')
)
));
Restricting to books a user he has bought
This rule is also easy to implement, thought slightly more involved:
$BookModelInstance->bindModel(array(
'hasOne' => array( // Yes, hasOne not hasMany
'MyPurchase' => array(
'className' => 'Purchase',
'foriegnKey' => 'user_id'
)
)
));
$results = $BookModelInstance->find('all', array(
'recursive' => 0, // to join hasOne+belongsTo associations into the query
'conditions' => array(
'MyPurchase.user_id' = AuthComponent::user('id'),
)
));
The bindModel call achieves the equivalent of SELECT .. FROM books LEFT JOIN book_users.. The conditions in the find call will therefore restrict results to books where there is a record of the user purchasing the book.
Putting them both together
A simplistic implementation of automatically applying both these rules would be:
model Book extends AppModel {
public $actsAs = array('Containable');
public $restrictToUser = true;
public function beforeSave($options = array()) {
if (!$this->id) {
// Store who created this book
$this->data[$this->alias]['created_by'] = AuthComponent::user('id');
}
return true;
}
public function beforeFind($queryData) {
if (!$this->restrictToUser) {
// we don't want to apply user-level restrictions
return true;
}
$userId = AuthComponent::user('id');
if (!$userId) {
// we want to restrict to the current user - there isn't one.
return false;
}
// define the association to the purchase table
$this->bindModel(array(
'hasOne' => array(
'MyPurchase' => array(
'className' => 'Purchase',
'foriegnKey' => 'user_id'
)
)
));
//ensure the purchase table is included in the current query
$queryData['contain']['MyPurchase'] = array();
// restrict to rows created by the current user, OR purchased by the current user
$queryData['conditions']['OR'] = array(
$this->alias '.created_by' => $userId,
'MyPurchase.user_id' => $userId
);
return $queryData;
}
}
This requires a field created_by (or equivalent) to be in the books table, and uses containable to ensure that the purchases table (or equivalent) is included in all relevant queries.
Most simple solution: Add a condition in your controller, so:
$this->set('books', $this->Book->find(
'all',
array('conditions' => array('Book.user_id' => $user['User']['id']))
);
Disadvantages: You will likely create duplicate code here since this check has to happen also in other places. Also when you start testing your model you can only test that it returns books, you cannot test a model method like: getMyBooks($userId). So no, not the preferred solution.
Next solution: Check in the model
It could be done by a check in for example your books model. You could just check in the afterfind() method whether the returned records are allowed or not. In your beforefind you could also add an additional condition to all queries.
In general a model should be fat so I would suggest implementing clear methods there like: getAllBooks, getBooksOfUser($User), getLatestBooksOfUser($User) etc.
Why is this a nice implementation? Because you now manage the access levels in a central place. You can test the model and you are sure it does only return books from this user.
With beforeSave etc. you can intervene every save attempt and first check: hey, you want to save this but is this really your book?
ACL solution
But in general it could be wise to implement some ACL solution (preferably the built in one) since that makes you application much more future proof. It allows flexibility, for example:
Each user may have access to a certain set of books. I don't need various roles.
That's true for now but the future can change it. So if you need a quick solution just custom filter the records. But think about the future.
In CakePHP, I have two tables, Countries & Networks. They have a HABTM relationship and are joined by countries_networks.
I'm trying to get all countries from the countries table where the 'name' field in Networks = 'o2'
I've realised I can't do this using a basic find(), so I've been experimenting with the containable behaviour. I have managed to restrict the returned data, but it looks as though 'containable' doesn't exactly work as I want. Heres my code:
$countries = $this->Country->find('all', array('contain' => array(
'Network' => array(
'conditions' => array('Network.name =' => "o2"),
)
)));
This query however returns ALL countries, and the Network.name if its 'o2'. What I really need to do is return ONLY the countries that have a Network.name of 'o2', and no others.
Can anyone help? Thanks.
"=' =>"
What is it? there is no needed to use "=" symbol after "Network.name"
Your query returns you exactly what your ask.
Try to select from Network.
$countries = $this->Country->Network->find('first', array(
'conditions' => array('Network.name' => "o2"),
'contain' => array('Country')
));
$countries = $countries['Country'];
You should be able to do something like this:
$this->Country->hasAndBelongsToMany['Network']['conditions'] = array('Network.name'=>'o2');
$myCountries = $this->Country->find('all');
I haven't tested this, but it should get you pretty close to where you need to be.
Also, bear in mind that changing the hasAndBelongsToMany array like this will affect all subsequent queries against that model during the current page load (but it will be reset the next time the model is instantiated).
I'm on my way out the door, so sorry for the brief explanation.
I think what you want is a HABTM relationship, but to be able to filter based on the associated model's data. To do this, check out the "Containable" behavior in Cake's manual. Pretty sure that's what you're after.