I'm adding some models to a project, and was wondering if there is a "best practice" kind of approach to creating models:
Does it make sense to create a function for each specific query?
I was starting to do this, then had the idea of creating a generic function that I could pass parameters to. e.g:
Instead of
function getClients(){
return $this->db->query('SELECT client_id,last FROM Names ORDER BY id DESC');
}
function getClientNames($clid){
return $this->db->query('SELECT * FROM Names WHERE client_id = '.$clid);
}
function getClientName($nameID){
return $this->db->query('SELECT * FROM Names WHERE id ='.$nameID);
}
}
Something like
function getNameData($args,$cond){
if($cond==''){
$q=$this->db->query('SELECT '.$args.' FROM Names');
return $q;
}else{
$q=$this->db->query('SELECT '.$args.' FROM Names WHERE '.$cond);
return $q;
}
}
where I can pass the fields and conditions (if applicable) to the model. Is there a reason the latter example would be a bad idea?
Thanks!
I think it would actually be a better idea to use CI's Active Record to compile the queries.
An example:
function all_clients($select)
{
$this->db->select($select);
return $this->_get_client_data();
}
function single_client($select, $id = "")
{
// validate $id
$this->db->select($select);
$this->db->where("id", $id);
$this->db->limit(1);
return $this->_get_client_data();
}
// Only called by a method above once the query parameters have been set.
private function _get_client_data()
{
$q = $this->db->get("clients");
if($q->num_rows() > 0)
{
return $q->result_array();
}
return FALSE;
}
CI's Active Record makes all the stuff you were wanting to much easier. You can imagine setting up your public functions to conditionally set a number of options before actually calling $this->db->get().
I guess you would call _get_client_data a catch-all (?) and running all your data retrieval through a single method makes stuff like error handling much easier to maintain.
NOTE: Always remember to validate data like this. I know you do, but I'm just repeating it.
Related
I have a (relatively) basic need in Nova that I can't seem to figure out and I slowly start to feel that I'm approaching things the wrong way.
So, I've got a User, Company, Device and Transfer models and respectively resources, everything pretty default regarding the resource setup.
The schema is the following:
users: id, company_id
companies: id, type_id, name where type_id is pointing to one of three pre-populated types (manufacturer, dealer, client)
devices: id, imei
transfers: id, from_company_id, to_company_id, accepted_at
and Transfer is in a Many-to-Many with Device.
The idea behind the transfers being that Manufacturers transfer to Dealers, Dealers transfer to Clients, so it's really only a one-way thing.
Now the problem occurs at the following crucial point in the logic:
In my Transfer resource pages, I want to show different fields depending on the type of the company the currently authenticated user belongs to. Basically, if the company is:
Manufacturer, then display a DEALER column populated with the transfers' toCompany relation;
Dealer, then display a CONTRAGENT column populated with the transfers' fromCompany or toCompany relations (depending on which mathces the current auth() company)
Client, then display a DEALER column populated with the transfers' fromCompany
All of the described logic works fine with the following code (App\Nova\Transfer.php as is) UNTIL I wanted to finally display the transfer's devices on the details page:
<?php
namespace App\Nova;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Fields\BelongsToMany;
use Laravel\Nova\Http\Requests\NovaRequest;
class Transfer extends Resource
{
public static $model = \App\Models\Transfer::class;
public static $title = 'id';
public static $search = [
'id',
];
public static $with = [
'fromCompany',
'toCompany'
];
public function fields(Request $request)
{
$company = auth()->company();
if($company->hasType('manufacturer'))
{
$contragentTitle = 'Dealer';
$contragent = 'toCompany';
}
else if($company->hasType('dealer'))
{
//\Debugbar::info($this); //showing empty resource when populating the devices
$contragentTitle = 'Contragent';
$contragent = $this->fromCompany->is($company) ? 'toCompany' : 'fromCompany'; //exception here, since the resource is empty and fromCompany is null
}
else
{
$contragentTitle = 'Dealer';
$contragent = 'fromCompany';
}
$contragentCompanyField = BelongsTo::make("$contragentTitle company", $contragent, Company::class);
if($company->hasType('dealer'))
{
$contragentCompanyField->displayUsing(function ($contragentCompany) use ($contragent){
return $contragentCompany->title() . " (".($contragent == 'toCompany' ? 'Outgoing' : "Incoming").')';
});
}
return [
ID::make(__('ID'), 'id')->sortable(),
$contragentCompanyField,
BelongsToMany::make('Devices') //problematic field, when removed, everything is fine...
];
}
public static function indexQuery(NovaRequest $request, $query)
{
if(auth()->check())
{
return $query->where(function($subQuery){
return $subQuery->where('from_company_id', auth()->company()->id)->orWhere('to_company_id', auth()->company()->id);
});
}
}
public function cards(Request $request)
{
return [];
}
public function filters(Request $request)
{
return [];
}
public function lenses(Request $request)
{
return [];
}
//action is working fine (additional canRun added to avoid policy conflicts)
public function actions(Request $request)
{
return [
(new Actions\AcceptTransfer())->showOnTableRow()->canSee(function ($request) {
if ($request instanceof \Laravel\Nova\Http\Requests\ActionRequest) {
return true;
}
return $this->resource->exists
&& $this->resource->toCompany->is(auth()->company())
&& $this->resource->accepted_at === null;
})->canRun(function ($request) {
return true;
})
];
}
}
Now the strange thing that is happening is that the fields() method gets called multiple times on multiple ajax requests behind the scenes with Nova and when populating the devices relationship table, it gets called without a resource, although a call is never actually needed (as far as I can grasp the mechanics behind Nova) or at least when fetching relationships, you must still have the model information (at least the ID) somewhere to fetch by... So basically, if I'm a user of a dealer company, I can't see the devices that are being transferred (currently throwing a calling is() on null exception).
Now, this happens to be a big problem, since it hinders most of the stuff I need for my transfers, but also generally I don't like my approach so far, so... What would be the right way to achieve this multi-layer resource? Ideally I'd like to define three different transfer resource classes and somehow tell nova which one to use based on the user's company's type (since branching will most probably just grow more complex and therefore uglier as of the current aproach), but I can't figure out the way to do so.
I've also considered moving this entire logic to a separate Nova tool, but I really don't know much about them yet and whether that would be the right option... The only thing stopping me is that I still won't be able to elegantly solve the multi-layer problem and will have to write much of the otherwise useful Nova CRUD logic and views myself...
Any explanations (regarding the multiple calls of fields() and why resource is empty) or general structural recommendations to solve this case would be greatly appreciated! Many thanks in advance!
EDIT:
I was able to circumvent the error by taking advantage of viaResourceId, so instaed of $this I ended up using:
$transfer = $this->id ? $this->resource : \App\Models\Transfer::find($request->viaResourceId);
but the messy code and the unneeded calls still remain an open question. Thanks again in advance!
Here is an example of how I handled this:
public function fields(NovaRequest $request)
{
/** #var \App\Models\User $user */
$user = $this->id ? $this->resource : \App\Models\User::find($request->viaResourceId);
if ($user && $user->whatEver()) {
// display special fields in preview/detail view
return [...];
}
// display for index and if no model is found
return [...];
}
How to explicitly say to route model binding to fetch only related categories? I have my web.php file as follows:
Route::get('/catalog/{category}', [CategoryController::class, 'index'])->name('category.index');
Route::get('/catalog/{category}/{subcategory}', [SubcategoryController::class, 'index'])->name('subcategory.index');
Route::get('/catalog/{category}/{subcategory}/{subsubcategory}', [SubsubcategoryController::class, 'index'])->name('subsubcategory.index');
Subsubcategory controller:
public function index(Category $category, Subcategory $subcategory, Subsubcategory $subsubcategory)
{
$subsubcategory->load('product')->loadCount('product');
$products = Product::where('subsubcategory_id', $subsubcategory->id)->orderByRaw('product_order = 0, product_order')->get();
return view('subsubcategory.index', compact('subsubcategory', 'products'));
}
And model in question:
public function subcategory()
{
return $this->belongsTo(Subcategory::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function getRouteKeyName()
{
return 'slug';
}
It works partially ok. It loads all the slugs, but the problem is, let's say I have Samsung Subsubcategory with it's parent categories like:
catalog/mobile-phones/android/samsung
Whenever I modify url from catalog/mobile-phones/android/samsung to catalog/mobile-phones/ios/samsung it works, where in fact it should not. How to handle this second scenario?
PS: it also applies if I open subcategory and change category slug. But, obviously, if upper level category does not exists, it's going to throw 404.
You may want to explore the docs a bit in regard to explicit route model binding and customizing the resolution logic to get some ideas.
https://laravel.com/docs/8.x/routing#customizing-the-resolution-logic
The following is untested and I'm making some guesses about your table structures, but I think this should give you a basic concept of how you can alter route model binding to fit your needs. The same concept could also be applied to the {subcategory} binding, but with one less relationship check.
App/Providers/RouteServiceProvider.php
public function boot()
{
// ...default code...
// add custom resolution for binding 'subsubcategory'
Route::bind('subsubcategory', function($slug, $route) {
// check to see if category exists
if ($category = Category::where('slug',$route->parameter('category'))->first()) {
// check to see if subcategory exists under category
if ($subcategory = $category->subcategories()->where('slug',$route->parameter('subcategory'))->first()) {
// check to see if subsubcategory exists under subcategory
if ($subsubcategory = $subcategory->subsubcategories()->where('slug',$slug)->first()) {
// success, proper relationship exists
return $subsubcategory;
}
}
}
// fail (404) if we get here
throw new ModelNotFoundException();
});
}
I will note, however, that this makes a number of separate database calls. There may be more efficient ways to achieve the same goal through other methods if optimization is a concern.
I have a question about obtaining parameters from Request object.
What is the difference between
$name = $request->name;
OR
$name = $request->input("name");
They show the same behavior. I am asking that from the typing perspective, it is faster to utilize #1 method. But I don't know the difference. Is #1 prone to SQL injections?
Basically, the first case is just a syntactic sugar for the second. In Laravel, Request implements __get magic function to access its internal properties.
public function all()
{
return array_replace_recursive($this->input(), $this->allFiles());
}
public function __get($key)
{
$all = $this->all();
if (array_key_exists($key, $all)) {
return $all[$key];
} else {
return $this->route($key);
}
}
In the first case, if any files were uploaded, Laravel first looks for a property amongst them. And if there is no such param in files or in input, in your first snippet, Laravel also looks for a value amongst route parameters:
To protect your code against SQL injections, you have to use prepared statements/query builder/ORM. You should not escape/change input, so both these functions don't protect you against SQL injections.
the following function is supposed to read the name of the given asset code from the database. but it triggers the error: "Trying to get property of non-object"
function sban_name($asset){
$this->db->select('name');
$this->db->from('asset_types');
$this->db->where('code',$asset);
return $this->db->get()->result()->row('name');
}
All I want is to have the name of the asset returned back to the controller! Your help is highly appreciated!
Use row() like,
return $this->db->get()->row()->name;
Use row() for a single row, and result() for multiple rows.
do like this, asset_types is your table name
function sban_name($asset){
$this->db->select('name');
$this->db->from('asset_types');
$this->db->where('code',$asset);
return $this->db->get('asset_types');
}
And in your controller acess it like
$result=$this->modelname->sban_name('$asset')->row();
$name=$result->name;
I think it's important to check if the record that satisfies the conditions even exists in the database. Code for the model:
function sban_name($asset){
$this->db->select('name');
$this->db->from('asset_types');
$this->db->where('code',$asset);
$row = $this->db->get()->row();
if (isset($row)) {
return $row->name;
} else {
return false;
}
}
Simply call the function from the controller like so:
$response = $this->model_name->sban_name($asset)
Try this code of block , I already checked and works fine:
function sban_name($asset)
{
$this->db->select('name');
$this->db->from('asset_types');
$this->db->where('code', $asset);
return $this->db->get()->row()->name;
}
i am new with codeigniter.
i have used the following code to execute query recursively.
Suppose $q query select 4 id (10,11,20,24)
then for each id showreply function (in foreach) call recursively then how can return the combine result.
$resultq3 = $this->showreply($reply_id);
<?php
public function showreply($reply_id)
{
$q1 =$this->db->select('*')
->from('forum_reply AS fr')
->where('fr.parent_id',$reply_id1)
->order_by('fr.id ')->get();;
foreach($q1->result_array() as $row4)
{
$id = $row4['id'];
$parent_id = $row4['parent_id'];
if($parent_id!=0)
{
$this->showreply($id);
}
}
return $result;
}
?>
I'm not really understanding what it is you're asking here. Maybe showing the showReply function would help, but you already have the combined result and are splitting that out in your foreach so what's the problem? Also why are you assigning reply_id to reply_id1? What is the point of that? Just use $reply_id in your query.
You're also executing an if statement that makes little sense since you can filter out the id's you don't want in the query itself (and are you seriously ever going to have an id that = 0?)
In fact the more I look at this code the more confused I become. Where is $id getting populated for $this->showreply($id)?
<?php
public function showreply($reply_id)
{
$q1 =$this->db->select('*')
->from('forum_reply AS fr')
->where('fr.parent_id',$reply_id)
->where('fr.parent_id !=',0)
->order_by('fr.id ')->get();;
//$i=0;
foreach($q1->result_array() as $row4)
{
$parent_id = $row4['parent_id'];
$this->showreply($id);
}
//below is the actual answer to your question on how to return the combined results.
return $q1->result_array();
}
?>
Okay after rereading your question I think I have a better understanding. If you pass the id's as an array like this:
$reply_id = array(10,11,20,24);
You can then modify your query to use:
$this->db->where_in('fr.parent_id', $reply_id);
That will return the results as one combined result with all 4 ids included.