im creating a management system where teachers can manage students final projects and the formers can see what other students created
im a laravel newbie and im having problems optimizing queries and validating urls
here are my table schemas:
Cursos
+-------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| curso | varchar(255) | NO | | NULL | |
+-------+------------------+------+-----+---------+----------------+
Trienios
+--------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| data_trienio | varchar(255) | NO | | NULL | |
| curso_id | int(11) | NO | | NULL | |
| oe_id | int(11) | NO | | NULL | |
+--------------+------------------+------+-----+---------+----------------+
Alunos
+------------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| id_cartao | int(10) unsigned | NO | UNI | NULL | |
| nome | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | UNI | NULL | |
| trienio_id | int(11) | NO | | NULL | |
+------------+------------------+------+-----+---------+----------------+
PAP
+-----------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| nome | varchar(255) | NO | | NULL | |
| descricao | text | NO | | NULL | |
| nota | int(11) | NO | | NULL | |
| aluno_id | int(11) | NO | | NULL | |
+-----------+------------------+------+-----+---------+----------------+
so far i've managed to set up dynamic urls based on the records defined on the cursos and trienios table, like this: http://localhost:8000/TGEI/2014-2017
(TGEI being a record in the cursos table that fetches the associated trienio records and 2014-2017 being a record in the trienios table that's related to a curso record in a 1-to-many relationship and fetches the related pap records)
this is all working nice and smooth, but i'm having trouble with optimizing hugely inefficient queries that will become a very significant problem when the database grows
here are my relationships:
Curso.php
public function trienio()
{
return $this->hasMany('App\Trienio');
}
Trienio.php
public function curso()
{
return $this->belongsTo('App\Curso');
}
public function oe()
{
return $this->belongsTo('App\OE');
}
public function aluno()
{
return $this->hasMany('App\Aluno');
}
Aluno.php
public function trienio()
{
return $this->belongsTo('App\Trienio');
}
public function pap()
{
return $this->hasOne('App\PAP');
}
PAP.php
protected $table = 'pap';
public function aluno()
{
return $this->belongsTo('App\Aluno');
}
and these are the controllers that are in charge of serving the user-accessible pages:
CursoController.php
public function index(Curso $curso)
{
$cursos = $curso->all();
return view('curso')->withCursos($cursos);
}
TrienioController.php
public function index(Trienio $trienio, $curso)
{
$trienios = $trienio->whereHas('curso', function ($query) use ($curso) {
$query->where('curso', '=', $curso);
})->get();
return view('trienio')->withTrienios($trienios);
}
PapController.php
public function index(Pap $pap, $curso, $trienio)
{
$pap = $pap->whereHas('aluno.trienio', function ($query) use ($curso, $trienio) {
$query->where('data_trienio', '=', $trienio)->whereHas('curso', function ($query) use ($curso) {
$query->where('curso', '=', $curso);
});
})->toSql();
dd($pap);
return view('pap')->withPap($pap);
}
public function show(Pap $pap, $curso, $trienio, $id)
{
$pap = $pap->find($id);
dd($pap);
return view('show')->withPap($pap);
}
as you can see, in the case of the index method of the PAP controller, the query that requests the data is a huge mess that is the epitome of the n+1 problem:
"select * from `pap` where exists (select * from `alunos` where `pap`.`aluno_id` = `alunos`.`id` and exists (select * from `trienios` where `alunos`.`trienio_id` = `trienios`.`id` and `data_trienio` = ? and exists (select * from `cursos` where `trienios`.`curso_id` = `cursos`.`id` and `curso` = ?)))"
what i intend with this query is to fetch the PAP records that are related to a trienio record, which in turn is related to a curso record, based on the input the user enters in the url (i've shown an example above), the problem is, as i'm a newbie to this stuff in general, i was unable to apply the eager loading concepts to the query i want to run
also i'm having a problem with validating urls in which the user can input the following:
http://localhost:8000/qwfkjnfwq/qjqtikjn/1
and the controller method show will fetch a pap record without regard to the parameters that the user inputed 2 levels above, and this obviously will pose a "security" problem
and what i wanted to do was:
http://localhost:8000/TGEI/2014-2017/1
the controller method show will load the aluno.trienio nested relationship, then fetch the trienio id related to the aluno model in accordance to the 2014-2017 parameter, then fetch the curso id related to the trienio model in accordance to the TGEI parameter
and so, stuff like this
http://localhost:8000/qwfkjnfwq/qjqtikjn/1
would be invalidated instead of going through.
this may be a tricky question but whoever that can help me, i would thank so. i understand that some parts of my question may be unclear(even more so because english isnt my first language) and in that case, i can clarify them all you want.
and for better information, here is my web.php file
Route::get('/', 'CursoController#index');
Route::get('/{curso}', 'TrienioController#index');
Route::get('/{curso}/{trienio}', 'PapController#index');
Route::get('/{curso}/{trienio}/{id}', 'PapController#show');
Okay to expand on my comment.
With Laravel 5.2 came route model binding which enables you to inject the model in the controller method (like this: public function show(Pap $pap)) and Laravel will automatically fetch the Pap model with the id in the url (bascially doing Pap::find($id) and saving the return into $pap variable). This is not always something you want, because often you want to perform more complex queries.
I would recommend you not to use the route model binding in you case and just do the queries on your own. Something like this (see how I've removed the Models from controller functions)
// CursoController.php
public function index()
{
$cursos = Curso::all();
return view('curso')->withCursos($cursos);
}
// TrienioController.php
public function index($curso)
{
$trienios = Trienio::whereHas('curso', function ($query) use ($curso) {
$query->where('curso', '=', $curso);
})->get();
return view('trienio')->withTrienios($trienios);
}
// Pap controller
public function index($curso, $trienio)
{
$pap = Pap::whereHas('aluno.trienio', function ($query) use ($curso, $trienio) {
$query->where('data_trienio', '=', $trienio)->whereHas('curso', function ($query) use ($curso) {
$query->where('curso', '=', $curso);
});
})->get();
return view('pap')->withPap($pap);
}
public function show($curso, $trienio, $id)
{
$pap = Pap::whereHas('aluno.trienio', function ($query) use ($curso, $trienio) {
$query->where('data_trienio', '=', $trienio)->whereHas('curso', function ($query) use ($curso) {
$query->where('curso', '=', $curso);
});
})->findOrFail($id);
return view('show')->withPap($pap);
}
Also note that in the show() method I've pretty much copied the index() query which is the validation.
And regarding the optimization of queries - the queries as you have them are absolutely fine. There's no n+1 problem as is.
You will have the n+1 problem if you will be performing a foreach on one of the index results and calling child's properties. For example if you will do something like this in a pap view:
#foreach($pap as $p)
<div>{{ $p->aluno->id }}</div>
#endforeach
This would make a new query for every $p in $pap to fetch the related aluno results.
To avoid this n+1 problem you have to load the data before using it in a loop. You would eager load the data using the ->with(relationship) method. Something like this:
// Pap controller
public function index($curso, $trienio)
{
$pap = Pap::whereHas('aluno.trienio', function ($query) use ($curso, $trienio) {
$query->where('data_trienio', '=', $trienio)->whereHas('curso', function ($query) use ($curso) {
$query->where('curso', '=', $curso);
});
})
->with('aluno.trienio') // You might need some additional checks here, depending on you needs
->get();
return view('pap')->withPap($pap);
}
It's not completely intuitive, but ->whereHas(relationship) will not eager load the relationship. So often you will find yourself writing statement like this:
// Pap controller
public function index($curso, $trienio)
{
$pap = Pap::whereHas('aluno.trienio', function ($query) use ($curso, $trienio) {
$query->where('data_trienio', '=', $trienio)->whereHas('curso', function ($query) use ($curso) {
$query->where('curso', '=', $curso);
});
})
->with(['aluno.trienio' => function ($q) use ($curso, $trienio) {
$query->where('data_trienio', '=', $trienio)->whereHas('curso', function ($query) use ($curso) {
$query->where('curso', '=', $curso);
}]); // These are the additional checks
->get();
return view('pap')->withPap($pap);
}
Related
I've encountered a mysterious error while testing my Laravel app: a model instance that's just been created is not found inside the middleware after I call postJson method.
The test class StoreEventControllerTest extends Laravel's TestCase, which is using LazilyRefreshesDatabase. I've tried removing it form the TestCase and also added RefreshDatabase, to no avail.
I've tried debugging the test, but still couldn't figure out what's going on:
Test debugging step #1: A model instance's been created
Test debugging step #2: The recently created model is not found!
Route is configured in routes/api.php:
Route::post('stores/{key}/receive-payload', StoreEventController::class)
->middleware('event.signature')
->name('stores.receive-payload');
The middleware event.signature handle method:
public function handle(Request $request, Closure $next)
{
$key = $request->route('key');
$sourceName = $request->get('source');
if (!$key || !$sourceName) {
abort(401);
}
/** #var EventStore $store */
if (!$store = EventStore::find($key)) {
abort(401);
}
/** #var EventStoreSource $source */
if (!$source = $store->sources()->where('name', $sourceName)->first()) {
abort(401);
}
$providedSignature = $this->getProvidedSignature($request, $source);
if (!$providedSignature) {
abort(401);
}
if (!$this->checkSignature($providedSignature, $request, $store, $source)) {
abort(401);
}
// Bind the event store to the request so that it can be used in the controller
app()->instance(EventStoreSource::class, $source);
return $next($request);
}
And here is a relevant excerpt from model EventStore:
use App\Models\Concerns\UsesUuidKey;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class EventStore extends Model
{
use HasFactory;
use UsesUuidKey;
namespace App\Models\Concerns;
use Illuminate\Support\Str;
trait UsesUuidKey
{
public function initializeUsesUuidKey()
{
$this->setKeyType('string');
$this->setIncrementing(false);
}
public static function bootUsesUuidKey()
{
static::creating(fn ($model) => $model->setUuidKey());
}
protected function setUuidKey()
{
$keyName = $this->getKeyName();
if (blank($this->getAttribute($keyName))) {
$this->setAttribute($keyName, Str::uuid());
}
}
}
The table event_stores structure:
+--------------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+---------------------+------+-----+---------+-------+
| id | varchar(191) | NO | PRI | NULL | |
| team_id | bigint(20) unsigned | YES | MUL | NULL | |
| name | varchar(191) | NO | | NULL | |
| secret | text | NO | | NULL | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
| activated_at | timestamp | YES | | NULL | |
| config | json | YES | | NULL | |
| sources | json | YES | | NULL | |
+--------------+---------------------+------+-----+---------+-------+
Here's some extra commands I ran on the debug console during the test, which demonstrate the database connection doesn't change during the whole process:
Edit: I've added the table description.
Edit 2: Added a screenshot of the debug console after running commands to print the DB connection name.
Problem
I created a simple friendship relationship for my Laravel app which all worked ok until I noticed that when I queried the friendship of a user it would only search the current user on the UID1 field.
Since friendships are in essence a two-way relationship, Im trying to find a way in a laravel Model to retrieve ALL friendships relations by multiple columns.
Current Implementation
public function friends()
{
return $this->belongsToMany( App\Modules\Users\Models\User::class ,'friends', 'uid1');
}
Ideal Implementation
public function friends()
{
$a = $this->belongsToMany( App\Modules\Users\Models\User::class ,'users_friends', 'uid1');
$b = $this->belongsToMany( App\Modules\Users\Models\User::class ,'users_friends', 'uid2');
return combine($a,$b);
}
Table Structure
+----------------------+
| users table |
+----------------------+
+----| id: primary UserID |
| | fname: string |
| +----------------------+
|
|
| +----------------------+
| | friends table |
| +----------------------+
| | id: primary iD |
| | |
+----| uid1: user_id |
| | |
+----| uid2: user_id |
+----------------------+
The current implementation will only result in 1 of these records returning if the Current UserID = 1 as per the data in the friends table below.
+-------------------------------+
| friends table (data) |
+--------|---------|------------+
| id | uid1 | uid2 |
+--------|---------|------------+
| 1 | 1 | 7 |
| 2 | 7 | 1 |
| 3 | 9 | 1 |
+-------------------------------+
User Model
<?php
namespace App\Modules\Users\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
protected $table = 'users';
protected $fillable = [
'username', 'email', 'password', .... .
];
public function friends()
{
return $this->belongsToMany( App\Modules\Users\Models\User::class ,'users_friends', 'uid1');
}
Environment
Server = Homestead/linux
PHP = 7
MySQL
Update
I have a FriendShip helper class I created which does something similar, however in this function I pass in the UserID explicitly
Friendship::where( [
[ 'uid1' ,'=', $uid],
])->orWhere( [
[ 'uid2', '=', $uid]
])->all();
You can add additional conditions when you're declaring relationship by simply chaining it.
<?php
//...
class User extends Model {
//...
public function friends() {
return $this->hasMany(/*...*/)->orWhere('uid2', $this->id);
}
//...
But keep in mind that eloquent is not grouping the first conditions of relation in parenthesis so you might end with SQL that will not work as expected in some cases (if using or, and should be fine)
For example the above might result in a SQL that looks like this
SELECT * FROM users_friends WHERE uid1 = ? AND uid1 IS NOT NULL OR uid2 = ?
Which is a correct SQL statement but without grouping you will not get the result that you're expecting.
Another way is to use accessor and two separate relationships
<?php
//...
public function friends1() {
return $this->belongsToMany(User::class, 'users_friends', 'uid1');
}
public function friends2() {
return $this->belongsToMany(User::class, 'users_friends', 'uid2');
}
public function getFriendsAttribute() {
return $this->friends1->merge($this->friends2);
}
//...
But this way you get two separate trips to DB.
I'm trying to get the output of a belongstoMany relationship between users and sections database, which are tied to User and Section model, respectively.
User model has this method:
public function section()
{
return $this->belongsToMany('App\Models\Section','user_section')->withTimestamps();
}
Section model has this method:
public function user()
{
return $this->belongsToMany('App\Models\User','user_section')->withTimestamps();
}
My user_section table looks like this:
+----+---------+------------+------------+------------+
| id | user_id | section_id | created_at | updated_at |
+----+---------+------------+------------+------------+
| 1 | 1 | 1 | NULL | NULL |
| 2 | 2 | 1 | NULL | NULL |
+----+---------+------------+------------+------------+
2 rows in set (0.00 sec)
So why when I do this in the business logic:
$user = User::where("id",Auth::user()->id)->first(); //user with id of 1 in my case
return json_encode($user->section());
Do I get {} as output?
$user->section() will return the BelongsToMany relationship instead of the model object itself. You can do $user->section instead to load the relationship and have the model returned.
I'm trying to obtain a three level relationship data, but I'm lost about it using laravel 5.1
I'll try to explain my scenario, hope you can help me.
I've got two models called Host and User.
This models are grouped by Hostgroup and Usergroup models, using a Many To Many relationship.
Then I've got a table called usergroup_hostgroup_permissions which relation an Usergroup with a Hostgroup:
+--------------+----------------------+------+-----+---------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+----------------------+------+-----+---------------------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| usergroup_id | int(10) unsigned | NO | MUL | NULL | |
| hostgroup_id | int(10) unsigned | NO | MUL | NULL | |
| action | enum('allow','deny') | NO | | allow | |
| enabled | tinyint(1) | NO | | 1 | |
+--------------+----------------------+------+-----+---------------------+----------------+
I'd like to obtain a list of users that belongs to an usergroup with a relation in this table with a hostgroup where my host belongs to.
For example:
My host_1 belongs to DEV_servers.
On usergroup_hostgroup_permissions table, there's an entry that allows developers to access to DEV_servers.
The usergroup developer has 3 users user_1, user_2 and user_3.
Any hint? Thanks in advance!
In order to obtain a list of users in a particular host, you need to nest all the underlying relationships (via .) using a whereHas method on the User model. i.e.
$users = User::whereHas('usergroup.hostgroups.hosts', function($q) use ($hostID){
$q->where('id', $hostID);
})->get();
Moreover, if you want to check against whether the user is allowed to access that particular host, then you may chain another where() to the above as such:
$users = User::whereHas('usergroup.hostgroups.hosts', function($q) use ($hostID){
$q->where('id', $hostID)->where('usergroup_hostgroup_permissions.action', 'allow');
})->get();
Note: If you are warned on an ambiguous id field, try including the table hosts to which the id belongs to as well, i.e. hosts.id.
I am assuming that you have defined the relations for those models as follows:
class HostGroup extends Eloquent {
/** ...
*/
public function hosts(){
return $this->hasMany('App\Host');
}
public function usergroups(){
return $this->belongsToMany('App\UserGroup', 'usergroup_hostgroup_permissions');
}
}
class Host extends Eloquent {
/** ...
*/
public function hostgroup() {
return $this->belongsTo('App\HostGroup');
}
}
class UserGroup extends Eloquent {
/** ...
*/
public function users(){
return $this->hasMany('App\User');
}
public function hostgroups(){
return $this->belongsToMany('App\HostGroup', 'usergroup_hostgroup_permissions');
}
}
class User extends Eloquent {
/** ...
*/
public function usergroup(){
return $this->belongsTo('App\UserGroup');
}
}
Hope it turns out to be helpful.
I need a Codeigniter 2.x ACL + Authentication library.
I need to give have 3 different admin users and 2 different front end users and want to set everything dynamically through database.
Please help.
The two most popular authentication libraries for CI (at least as of early this year) seemed to be Ion_auth and Tank_auth. Neither handle your ACL needs, though Ion_auth provides single group functionality.
I started with Tank_auth on a project several months ago, but switched to Ion_auth when I needed more flexibility. With it's included functionality, I added a user_groups table and the necessary library and model functions to allow multiple group memberships for each user.
The data structure:
mysql> describe auth_users_groups;
+------------+-----------------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+-----------------------+------+-----+-------------------+----------------+
| id | int(11) unsigned | NO | PRI | NULL | auto_increment |
| user_id | mediumint(8) unsigned | NO | | NULL | |
| group_id | mediumint(8) unsigned | NO | | NULL | |
| dt_updated | timestamp | NO | | CURRENT_TIMESTAMP | |
+------------+-----------------------+------+-----+-------------------+----------------+
4 rows in set (0.00 sec)
Some of the code added in the library:
public function get_user_groups($user_id = NULL)
{
if ($user_id === NULL) $user_id = $this->get_user()->id;
return $this->ci->ion_auth_model->get_user_groups($user_id)->result();
}
/**
* is_member - checks for group membership via user_groups table
*
* #param string $group_name
* #return bool
**/
public function is_member($group_name)
{
$user_groups = $this->ci->session->userdata('groups');
if ($user_groups)
{
// Go through the groups to see if we have a match..
foreach ($user_groups as $group)
{
if ($group->name == $group_name)
{
return true;
}
}
}
// No match was found:
return false;
}
Some of the model code:
public function get_user_groups($user_id = NULL)
{
if ($user_id === NULL) return false;
return $this->db->select('group_id, name, description, dt_updated')
->join($this->tables['groups'], 'group_id = '.$this->tables['groups'].'.id', 'left')
->where('user_id', $user_id)
->get($this->tables['users_groups']);
}
public function set_group($user_id, $group_id)
{
$values = array('user_id'=>$user_id, 'group_id'=>$group_id);
$hits = $this->db->where($values)->count_all_results($this->tables['users_groups']);
if ($hits > 0)
{
return NULL;
}
return $this->db->insert($this->tables['users_groups'], $values);
}
public function remove_groups($user_id, $group_ids)
{
$this->db->where('user_id', $user_id);
$this->db->where_in('group_id', $group_ids);
return $this->db->delete($this->tables['users_groups']);
}