L5 How to use trait that hashes id but keep pivot functionality - php

I added hashes to my ID's using a trait. However by doing that now I can no longer use attach() or relationships.
For example this relationship does not work in my view anymore:
#foreach ($invoice->items as $item)
{{ $item->item }}
#endforeach
Here is the trait that hashes the id for me
<?php
namespace App\Traits;
use Hashids\Hashids;
use Illuminate\Database\Eloquent\Builder;
trait HashedId
{
/**
* Get the user's id as hashids.
*
* #param $value
* #return string
*/
public function getIdAttribute($value)
{
$hashids = new \Hashids\Hashids(env('APP_KEY'),10);
return $hashids->encode($value);
}
public function scopeHashId(Builder $query, $id)
{
$hashIds = new Hashids(env('APP_KEY'), 10);
$id = $hashIds->decode($id)[0];
return $query->where('id', $id);
}
}
Invoice Model:
<?php
namespace App;
use App\Traits\HashedId;
use Illuminate\Database\Eloquent\Model;
use HipsterJazzbo\Landlord\BelongsToTenants;
class Invoice extends Model
{
use BelongsToTenants;
use HashedId;
//
protected $fillable = [
'client_id',
'invoice_number',
'purchase_order',
'invoice_note',
'invoice_status',
'invoice_total',
'invoice_type',
'sub_total',
'balance_due',
'due_date',
'invoice_type',
'user_id',
];
protected $hidden = [
'user_id'
];
public function items()
{
return $this->belongsToMany('App\LineItem', 'invoice_items', 'invoice_id', 'item_id');
}
public function client()
{
return $this->belongsTo('App\Client');
}
}
I have tried doing this from a controller but it feels more like a hack than the right way to do it and I still lose the ability to use things like $invoice->attach($lineItem) or $invoice->items
//Currently I have to unhash the ids in order to save them as a pivot
$hashIds = new \Hashids\Hashids(env('APP_KEY'), 10);
$invoiceId = $hashIds->decode($request->invoice_id)[0];
$lineItemId = $hashIds->decode($request->item_id)[0];
//Should have been able to use $invoice->attach($lineItemId)
DB::table('invoice_items')->insert(
['invoice_id' => $invoiceId, 'item_id' => $lineItemId]
);
How can I continue to use $invoice->attach($lineItem) or $invoice->items from controllers while still using the trait that hashes my ids?

I've re-written the trait as follows (this assumes you're using PHP 5.6 or above):
<?php
namespace App\Traits;
use Hashids\Hashids;
use Illuminate\Database\Eloquent\Builder;
trait HashedId
{
/**
* Get model ID attribute encoded to hash ID.
*
* #return string
*/
public function getHashIdAttribute()
{
$hashIds = new Hashids(env('APP_KEY'), 10);
return $hashIds->encode($this->getKey());
}
/**
* Restrict query scope to find model by encoded hash ID.
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #param integer $id
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeHashId(Builder $query, $id)
{
$hashIds = new Hashids(env('APP_KEY'), 10);
$id = $hashIds->decode($id)[0];
return $query->where('id', $id);
}
/**
* Restrict query scope to find models by encoded hash IDs.
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #param array $ids
* #return \Illuminate\Database\Eloquent\Builder
*/
public function scopeHashIds(Builder $query, ...$ids)
{
$hashIds = new Hashids(env('APP_KEY'), 10);
$ids = array_map(function ($id) use ($hashIds) {
return $hashIds->decode($id)[0];
}, $ids);
return $query->whereIn('id', $ids);
}
}
You may notice that I've renamed the accessor, getIdAttribute() to getHashIdAttribute(). You can therefore now get the hash ID of a model instance by calling $model->hash_id instead of $model->id.
This is where I think your problem was, because Laravel was expecting an integer key to be returned by $model->id, whereas it would have been getting the hash ID instead.
If after implementing the changes above you're still getting an error, can you show what the specific error is?

Just like you commented, you couldn't use attach because id is hashed cos of getIdAttribute. I would like to suggest you to use getOriginal().
For example,
$invoice->attach($lineItem->getOriginal()['id']);
I think that could be the only way to attach that.

Related

Passing parameters in an Unit Test, function update in database CRUD Laravel

I've been creating some tests to try my create delete edit functions on laravel from my database, this is my code:
ConstituencyController.php :
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreConstituencyRequest;
use App\Http\Resources\ConstituencyResource;
use App\Models\Constituency;
use Illuminate\Http\Request;
use phpDocumentor\Reflection\Types\Collection;
class ConstituencyController extends Controller
{
/**
* Display a listing of the constituencies.
*
*
*/
public function index()
{
$constituency = Constituency::all();
return ConstituencyResource::collection($constituency);
}
/**
* Show the form for creating a new resource.
*
*
*/
public function create()
{
//
}
/**
* Store a newly created constituency in storage.
*
* #param Request $request
*
*/
public function store(Request $request)
{
$name = $request->name;
$data = array("name"=>$name);
Constituency::insert($data);
}
/**
* Display the specified constituency.
*
* #param int $id
*
*/
public function show(int $id)
{
$constituency = Constituency::find($id);
return new ConstituencyResource($constituency);
}
/**
* Show the form for editing the specified resource.
*
* #param int $id
*
*/
public function edit(int $id)
{
//
}
/**
* Update the specified constituency in storage.
*
* #param Request $request
* #param int $id
*
*/
public function update(Request $request, int $id)
{
$constituency = Constituency::find($id);
$constituency->name = $request->name;
$constituency->update();
}
/**
* Remove the specified constituency from storage.
*
* #param int $id
*
*/
public function destroy(int $id)
{
Constituency::find($id)->delete();
}
}
Constituency.php:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Constituency extends Model
{
use HasFactory;
public function candidate()
{
return $this->hasMany(Candidate::class);
}
public function town()
{
return $this->hasMany(Town::class);
}
}
ConstituencyResource.php :
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ConstituencyResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
ConstituencyFactory.php :
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* #extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Constituency>
*/
class ConstituencyFactory extends Factory
{
/**
* Define the model's default state.
*
* #return array<string, mixed>
*/
public function definition()
{
return [
'name' => $this->faker->word(),
];
}
}
Now this is my test to update a constituency:
public function test_a_constituency_can_be_modified()
{
$constituency = Constituency::factory()->create();
$constituency_id = $constituency->id;
$response = $this->put('api/constituencies/'.$constituency_id);
$this->assertDatabaseHas('constituencies', [
'id' => $constituency->id,
'name' => $constituency->name,
'created_at' => $constituency->created_at,
'updated_at' => $constituency->updated_at,
]);
}
Now of course the test passes, but i'm not actually giving it some new parameters to change... I've been trying to give some parameters to the function to actually change some data but i can't figure out how to do that.... I don't think i'm gonna have to put the parameters in the URI but where then?
If you are using PHPUnit you likely want to make use of Data Providers:
Example from docs
/**
* #dataProvider additionProvider
*/
public function testAdd(int $a, int $b, int $expected): void
{
$this->assertSame($expected, $a + $b);
}
public function additionProvider(): array
{
return [
'adding zeros' => [0, 0, 0],
'zero plus one' => [0, 1, 1],
'one plus zero' => [1, 0, 1],
'one plus one' => [1, 1, 3]
];
}
The smart folks over at Tighten also have an excellent tutorial on data providers.
If you're using PEST then you'll want Data Sets.
Example from docs
dataset('emails', [
'enunomaduro#gmail.com',
'other#example.com'
]);
it('has emails', function ($email) {
expect($email)->not->toBeEmpty();
})->with('emails'); // <-- use the dataset
Using data providers and data sets allows you to reuse data, but also test against multiple inputs for your unit test. You could if you wanted just hard code a value after you're arrange statement (where you create the DB record) but that has limitations and providers are far more flexible.
Update - Example test
The following is an example of how you might go about things. Note this is not exhaustive and things like using $request->all() to update your model are not advisable but I have done so to keep things simple for illustritive purposes. This should give you an idea of where/how you could go about performing your testing. There are many ways/opinions on such things.
api.php
Route::put('/constituencies/{constituency}',
[ConstituencyController::class, 'update']
)->name('api.constituencies.update');
ConstituencyController.php
<?php
namespace App\Http\Controllers;
use App\Models\Constituency;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ConstituencyController extends Controller
{
public function update(Request $request, Constituency $constituency)
{
$constituency->update($request->all());
return response()->json($constituency, Response::HTTP_OK);
}
}
ExampleTest.php
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\Constituency;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Foundation\Testing\RefreshDatabase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*
* #test
* #dataProvider constituencyNameProvider
* #return void
*/
public function it_can_update_constituency_name_successfully($constituencyName)
{
// Arrange
$constituency = Constituency::factory()->create();
$payload = ['name' => $constituencyName];
// Act
$response = $this->put(route('api.constituencies.update', $constituency->id), $payload);
// Assert
$response->assertStatus(Response::HTTP_OK)
->assertJson([
'id' => $constituency->id,
'name' => $constituencyName
])
->assertJsonStructure([
'id', 'name', 'created_at', 'updated_at'
]);
}
public function constituencyNameProvider(): array
{
return [
['Ostwald'],
['Springtown'],
['Baybarrow'],
['Blackhaven'],
['Lochspring'],
];
}
}

relatableQuery() for two resource fields on same model in Laravel Nova

I have a relationship between work 'days' and projects of different types. So my 'days' record has a reference to my 'projects' table twice because I have two different types of projects called 'Series' and 'Event'.
In my 'days' resource I've created two fields as such:
BelongsTo::make('Series','series',Project::class)->sortable()->nullable(),
BelongsTo::make('Event','event',Project::class)->sortable()->nullable(),
What I'm trying to do is filter the projects by their types so I've created this:
public static function relatableProjects(NovaRequest $request, $query){
return $query->where('type', 'Series');
}
I've tried making relatableSeries and relatableEvents but they don't work. How can I make this connect to the fields correctly without having to create two separate tables for 'series' and 'events'.
The relatableQuery above winds up filtering both resource fields.
Because relatableQuery() is referencing a relatableModel() (so relatableProjects() references the Project model) I was able to create another model solely for the purpose of helping with this.
I created an Event model which references the same projects table and then was able to create a relatableEvents() method to use the where() filter query.
Note: I did have to also create an Event resource which references the Event model since this is how Nova works but was able to hide it from being accessed which you can find more information about here
See revised BelongsTo fields and new model below:
Day resource
/**
* Build a "relatable" query for the given resource.
*
* This query determines which instances of the model may be attached to other resources.
*
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public static function relatableProjects(NovaRequest $request, $query){
return $query->where('type', 'Series');
}
/**
* Build a "relatable" query for the given resource.
*
* This query determines which instances of the model may be attached to other resources.
*
* #param \Laravel\Nova\Http\Requests\NovaRequest $request
* #param \Illuminate\Database\Eloquent\Builder $query
* #return \Illuminate\Database\Eloquent\Builder
*/
public static function relatableEvents(NovaRequest $request, $query){
return $query->where('type', 'Event');
}
/**
* Get the fields displayed by the resource.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function fields(Request $request)
{
return [
ID::make()->hideFromIndex()->hideFromDetail()->hideWhenUpdating(),
BelongsTo::make('User','user',User::class)->sortable(),
BelongsTo::make('Budget','budget',Budget::class)->sortable()->nullable(),
BelongsTo::make('Series','series',Project::class)->sortable()->nullable(),
BelongsTo::make('Event','event',Event::class)->sortable()->nullable(),
DateTime::make('Last Updated','updated_at')->hideFromIndex()->readOnly(),
new Panel('Schedule',$this->schedule()),
new Panel('Time Entry',$this->timeEntries()),
];
}
Event model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Event extends Model
{
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'projects';
protected $casts = [
'starts_on' => 'date',
'ends_on' => 'date',
];
public function event(){
return $this->belongsTo('App\Event');
}
public function project(){
return $this->belongsTo('App\Project');
}
}
i know it is an old question but i was facing same problem with laravel nova belongsto field, in some resource i have a belongsto that relates to users but these should be of 'supervisor' role in another resource i hace a belongsto field relating to users but these should be of role 'guard', as laravel nova belongsto field just takes all users in both selects all users appeared and it seems nova belongsto field doe not have a way, or at least i did not find it to scope the query, so what i did was creating a php class named BelongstoScoped this class extends laravel nova field BelongsTo so i overwrote the method responsible of creating the query
<?php
namespace App\Nova\Customized;
use Laravel\Nova\Query\Builder;
use Laravel\Nova\Fields\BelongsTo;
use Laravel\Nova\Http\Requests\NovaRequest;
class BelongsToScoped extends BelongsTo
{
private $modelScopes = [];
//original function in laravel belongsto field
public function buildAssociatableQuery(NovaRequest $request, $withTrashed = false)
{
$model = forward_static_call(
[$resourceClass = $this->resourceClass, 'newModel']
);
$query = new Builder($resourceClass);
//here i chaned this:
/*
$query->search(
$request, $model->newQuery(), $request->search,
[], [], ''
);
*/
//To this:
/*
$query->search(
$request, $this->addScopesToQuery($model->newQuery()), $request->search,
[], [], ''
);
*/
//The method search receives a query builder as second parameter, i just passed the result of custom function
//addScopesToQuery as second parameter, thi method returns the same query but with the model scopes passed
$request->first === 'true'
? $query->whereKey($model->newQueryWithoutScopes(), $request->current)
: $query->search(
$request, $this->addScopesToQuery($model->newQuery()), $request->search,
[], [], ''
);
return $query->tap(function ($query) use ($request, $model) {
forward_static_call($this->associatableQueryCallable($request, $model), $request, $query, $this);
});
}
//this method reads the property $modelScopes and adds them to the query
private function addScopesToQuery($query){
foreach($this->modelScopes as $scope){
$query->$scope();
}
return $query;
}
// this method should be chained tho the field
//example: BelongsToScoped::make('Supervisores', 'supervisor', 'App\Nova\Users')->scopes(['supervisor', 'active'])
public function scopes(Array $modelScopes){
$this->modelScopes = $modelScopes;
return $this;
}
}
?>
In my users model i have the scopes for supervisors and guard roles like this:
public function scopeActive($query)
{
return $query->where('state', 1);
}
public function scopeSupervisor($query)
{
return $query->role('supervisor');
}
public function scopeSuperadmin($query)
{
return $query->role('superadmin');
}
public function scopeGuarda($query)
{
return $query->role('guarda');
}
So in the laravel nova resource i just included the use of this class
*remember the namespace depends on how you name your file, in my case i created the folder Customized and included the file there:
use App\Nova\Customized\BelongsToScoped;
In the fields in nova resource i used like this:
BelongsToScoped::make('Supervisor', 'supervisorUser', 'App\Nova\Users\User')
->scopes(['supervisor', 'active'])
->searchable()
So that way i could call the belongsto field in the nova resources which filter users depending on modle scopes.
I hope this helps someone, sorry if my English is not that good.

Showing name in url instead of id?

I am building a small app in Laravel, but I needed to use some help. I read similar tasks but I don't see them related to what I have.
The app that I'm building is a dictionary and I really don't get the part where words are displayed by id in the URL. I tried to revert the logic for the curds (store, update, delete, show) so that they perform actions using the word name and not the word id. That worked. The redirecting to route app/{name} after the actions does work too.
Now, I presume this is not how it needs to be done, when the curds aren't taking actions on the id?
I also read about slugs, but since I'm building a dictionary, don't understand why I need to separate the word by a "-"? It's a single word, not like posts. And why do I need to have an extra field in my form for the slug, as the slug must be equal to the name? I'm getting here an error: the slug->unique() field must be not null.
If somebody has the time, please explain to me what's the best way for making the names of the words being displayed in the URL. Thank you, I appreciate it.
Edit
Here is my code for the update crud:
public function update(UpdateWordRequest $request, $name)
{
Word::query()->where('name', $name)->firstOrFail()->update($request->input());
return redirect()->route('dictionary.show', ['name' => $name])->with('success', 'Edited successfully!');
}
It's working fine, but when I re-edit the word and change it to word2, with a number, I then get this error: Trying to get property 'name' of non-object.
What do I need to make of it? Thank you :)!
I think you should be able to use a trait to fix this issue.
I would create a trait call hasSlug and have the following code:
<?php
namespace App\Traits;
trait HasSlug
{
protected static function bootHasSlug()
{
static::creating(function ($model) {
$columnName = static::getSlugColumn();
$model->$columnName = str_slug($model->name); //convert your name to slug
});
}
public function getSlugAttribute()
{
$columnName = static::getSlugColumn();
return (string) $this->attributes[$columnName];
}
protected static function getSlugColumn()
{
if (isset(static::$slugColumn)) {
return static::$slugColumn;
}
return 'slug';
}
}
So now if you use HasSlug trait to your model every time you create a record it will go and look for name field from your model and create a slug out of it.
Be sure to apply unique constrain to your name field if needed.
For more robust have a look at spatie slug package https://github.com/spatie/laravel-sluggable
Here is what I finally come up with.
Word.php
I used as #usrNotFound suggested spatie/laravel-sluggable.
<?php
namespace App;
use Spatie\Sluggable\HasSlug;
use Spatie\Sluggable\SlugOptions;
class Word extends Model
{
use HasSlug;
/**
* #return SlugOptions
*/
public function getSlugOptions(): SlugOptions
{
return SlugOptions::create()
->generateSlugsFrom('name')
->saveSlugsTo('slug')
->slugsShouldBeNoLongerThan(20);
}
/**
* #param $query
* #param $slug
* #return mixed
*/
public function scopeFindBySlug($query, $slug)
{
return $query->where('slug', $slug)
->get();
}
}
WordController.php
<?php
namespace App\Http\Controllers;
use App\Word;
use App\Http\Requests\CreateWordRequest;
use App\Http\Requests\UpdateWordRequest;
class WordController extends Controller
{
/**
* Store a newly created resource in storage.
* #param CreateWordRequest $request
* #return mixed
*/
public function store(CreateWordRequest $request)
{
$word = Word::query()
->create($request->input());
return redirect()
->route('dictionary.show', $word->slug)
->with('success', 'Created successfully!');
}
/**
* Display the specified resource.
* #param $slug
* #return mixed
*/
public function show($slug)
{
$word = Word::whereSlug($slug)
->firstOrFail();
return view('word.show')
->with('word', $word);
}
/**
* Update the specified resource in storage.
* #param UpdateWordRequest $request
* #param $slug
* #return mixed
*/
public function update(UpdateWordRequest $request, $slug)
{
Word::whereSlug($slug)
->firstOrFail()
->update($request->input());
return redirect()
->route('dictionary.show', $slug)
->with('success', 'Edited successfylly!');
}
/**
* Remove the specified resource from storage.
* #param $slug
* #return mixed
*/
public function destroy($slug)
{
Word::whereSlug($slug)
->delete();
return redirect()
->route('dictionary.index')
->with('success', 'Deleted successfully');
}
}
Route.php
Route::get('/dictionary/{slug}', 'WordController#show')
->name('dictionary.show')
->where('slug', '[-A-Za-z0-9_-]+');
Creating, updating, destroying new words works fine. Eventually there is a small problem. When I want to update a particular word and change the name of it, after saving the word, I'm getting
Sorry, the page you are looking for could not be found.
and if I look at the url I see the old name instead of the new one. Has this to do with my update method and how do I fix it?

Laravel Eloquent Case Sensitive Relationships

Hi SO I'm having real issues with some Laravel Eloquent relationships which I can only guess are being caused by a case-sensitive relation and I'm hoping somebody here can help!
Here are the models that I'm having the issues with:
class DeliveryManifestLines extends Eloquent
{
protected $table = 'manifests';
public function sapDelivery()
{
return $this->hasOne('Delivery', 'DocNum', 'sap_delivery');
}
}
class Delivery extends Eloquent
{
protected $connection = 'sap';
protected $table = 'ODLN';
protected $primaryKey = 'DocNum';
public function deliveryManifest() {
return $this->belongsTo('DeliveryManifest', 'DocNum', 'sap_delivery');
}
public function address()
{
return $this->hasOne('Address', 'Address', 'ShipToCode')->where('CardCode', $this->CardCode)->where('AdresType', 'S');
}
public function geolocation()
{
return $this->hasOne('GeoLocation', 'Address', 'ShipToCode')->where('CardCode', $this->CardCode)->where('AdresType', 'S')->where('Lat', '>', 0)->where('Lng', '>', 0);
}
}
class Address extends Eloquent
{
protected $connection = 'sap';
protected $table = 'CRD1';
protected $primaryKey = 'Address';
public function delivery() {
return $this->belongsTo('Delivery', 'Address', 'ShipToCode');
}
}
Here's the code in my controller that is supposed to fetch some of the above models from the DB.
$deliveries = DeliveryManifestLines::with('sapDelivery')->where('manifest_date', $date))->get();
foreach ($deliveries as $delivery) {
$delivery->sapDelivery->load('address');
}
I'm using the "->load('address)" line as no matter what I tried I could not get eager loading to work with "sapDelivery.address"
In 99% of cases the address is loaded successfully from the DB but I have come across one case in which I am experiencing an issue that I can only think is being caused by case-sensitivity.
Using Laravel DebugBar I can see that my application is executing the following query:
SELECT * FROM [CRD1] WHERE [CardCode] = 'P437' AND [AdresType] = 'S' AND [CRD1].[Address] IN ('The Pizza Factory (topping)')
When I dump the contents of $delivery->sapDelivery in this occurrence the address relation is NULL, however, when I paste the SQL statement into my DB console and execute it manually I get the expected row returned.
The only difference I can see between this one address and the thousands of others that are working is that there is a case difference between the Address fields:
In the CRD1 table the Address field for the effected/expected row is "The Pizza Factory (Topping)" but the eloquent relationship is using AND [CRD1].[Address] IN ('The Pizza Factory (topping)') to try and find it I'm aware that SQL is case-insensitive be default but I can't think of any other reason why this one row is behaving differently to the others.
Does anybody have any other ideas as to what could be causing this issue and suggest any possible solutions or confirm either way my theory of case sensitivity being the culprit.
Many thanks!
So After giving this problem little thought over the past few months I revisited the issue today and found some very useful code on laravel.io by somebody experiencing the same issue I found myself with.
I've built on MattApril's solution to provide the least hacky way I can think of to provide a way to offer case insensitive relationships in laravel.
To achieve this you need to add a few new classes which utilise the strtolower() function to create lower case keys which allows the isset() function used in the relationships to find differently cased but matching keys:
ModelCI.php (app\Models\Eloquent\ModelCI.php)
<?php
namespace App\Models\Eloquent;
use App\Models\Eloquent\Relations\BelongsToCI;
use App\Models\Eloquent\Relations\HasManyCI;
use App\Models\Eloquent\Relations\HasOneCI;
use Illuminate\Database\Eloquent\Model;
abstract class ModelCI extends Model
{
/**
* Define a one-to-many relationship.
*
* #param string $related
* #param string $foreignKey
* #param string $localKey
*
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function hasManyCI($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related();
$localKey = $localKey ?: $this->getKeyName();
return new HasManyCI($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}
/**
* Define a one-to-one relationship.
*
* #param string $related
* #param string $foreignKey
* #param string $localKey
* #return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function hasOneCI($related, $foreignKey = null, $localKey = null)
{
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$localKey = $localKey ?: $this->getKeyName();
return new HasOneCI($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}
/**
* Define an inverse one-to-one or many relationship.
*
* #param string $related
* #param string $foreignKey
* #param string $otherKey
* #param string $relation
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function belongsToCI($related, $foreignKey = null, $otherKey = null, $relation = null)
{
// If no relation name was given, we will use this debug backtrace to extract
// the calling method's name and use that as the relationship name as most
// of the time this will be what we desire to use for the relationships.
if (is_null($relation))
{
list(, $caller) = debug_backtrace(false, 2);
$relation = $caller['function'];
}
// If no foreign key was supplied, we can use a backtrace to guess the proper
// foreign key name by using the name of the relationship function, which
// when combined with an "_id" should conventionally match the columns.
if (is_null($foreignKey))
{
$foreignKey = snake_case($relation).'_id';
}
$instance = new $related;
// Once we have the foreign key names, we'll just create a new Eloquent query
// for the related models and returns the relationship instance which will
// actually be responsible for retrieving and hydrating every relations.
$query = $instance->newQuery();
$otherKey = $otherKey ?: $instance->getKeyName();
return new BelongsToCI($query, $this, $foreignKey, $otherKey, $relation);
}
}
BelongsToCI.php (app\Models\Eloquent\Relations\BelongsToCI.php)
<?php namespace App\Models\Eloquent\Relations;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class BelongsToCI extends BelongsTo {
/**
* Match the eagerly loaded results to their parents.
*
* #param array $models
* #param \Illuminate\Database\Eloquent\Collection $results
* #param string $relation
* #return array
*/
public function match(array $models, Collection $results, $relation)
{
$foreign = $this->foreignKey;
$other = $this->otherKey;
// First we will get to build a dictionary of the child models by their primary
// key of the relationship, then we can easily match the children back onto
// the parents using that dictionary and the primary key of the children.
$dictionary = array();
foreach ($results as $result)
{
$dictionary[strtolower($result->getAttribute($other))] = $result;
}
// Once we have the dictionary constructed, we can loop through all the parents
// and match back onto their children using these keys of the dictionary and
// the primary key of the children to map them onto the correct instances.
foreach ($models as $model)
{
if (isset($dictionary[strtolower($model->$foreign)]))
{
$model->setRelation($relation, $dictionary[strtolower($model->$foreign)]);
}
}
return $models;
}
}
HasManyCI.php (app\Models\Eloquent\Relations\HasManyCI.php)
<?php namespace App\Models\Eloquent\Relations;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
class HasManyCI extends HasMany {
/**
* Build model dictionary keyed by the relation's foreign key.
*
* #param \Illuminate\Database\Eloquent\Collection $results
* #return array
*/
protected function buildDictionary(Collection $results)
{
$dictionary = array();
$foreign = $this->getPlainForeignKey();
// First we will create a dictionary of models keyed by the foreign key of the
// relationship as this will allow us to quickly access all of the related
// models without having to do nested looping which will be quite slow.
foreach ($results as $result)
{
$dictionary[strtolower($result->{$foreign})][] = $result;
}
return $dictionary;
}
/**
* Match the eagerly loaded results to their many parents.
*
* #param array $models
* #param \Illuminate\Database\Eloquent\Collection $results
* #param string $relation
* #param string $type
* #return array
*/
protected function matchOneOrMany(array $models, Collection $results, $relation, $type)
{
$dictionary = $this->buildDictionary($results);
// Once we have the dictionary we can simply spin through the parent models to
// link them up with their children using the keyed dictionary to make the
// matching very convenient and easy work. Then we'll just return them.
foreach ($models as $model)
{
$key = strtolower( $model->getAttribute($this->localKey) );
if (isset($dictionary[$key]))
{
$value = $this->getRelationValue($dictionary, $key, $type);
$model->setRelation($relation, $value);
}
}
return $models;
}
}
HasOneCI.php (app\Models\Eloquent\Relations\HasOneCI.php)
<?php namespace App\Models\Eloquent\Relations;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasOne;
class HasOneCI extends HasOne {
/**
* Match the eagerly loaded results to their many parents.
*
* #param array $models
* #param \Illuminate\Database\Eloquent\Collection $results
* #param string $relation
* #param string $type
* #return array
*/
protected function matchOneOrMany(array $models, Collection $results, $relation, $type)
{
$dictionary = $this->buildDictionary($results);
// Once we have the dictionary we can simply spin through the parent models to
// link them up with their children using the keyed dictionary to make the
// matching very convenient and easy work. Then we'll just return them.
foreach ($models as $model)
{
$key = strtolower($model->getAttribute($this->localKey));
if (isset($dictionary[$key]))
{
$value = $this->getRelationValue($dictionary, $key, $type);
$model->setRelation($relation, $value);
}
}
return $models;
}
/**
* Build model dictionary keyed by the relation's foreign key.
*
* #param \Illuminate\Database\Eloquent\Collection $results
* #return array
*/
protected function buildDictionary(Collection $results)
{
$dictionary = array();
$foreign = strtolower($this->getPlainForeignKey());
// First we will create a dictionary of models keyed by the foreign key of the
// relationship as this will allow us to quickly access all of the related
// models without having to do nested looping which will be quite slow.
foreach ($results as $result)
{
$dictionary[$result->{$foreign}][] = $result;
}
return $dictionary;
}
}
To utilise the new classes you must define a relationship as so:
$this->belongsToCI('Model');
or
$this->hasManyCI('Model');
or
$this->hasOneCI('Model');
Eloquent uses internal associative arrays to map and link the related records to their parents and in cases with string foreign keys and difference in uppercase vs lowercase, some of the related records will not be mapped.
I recently came across the same problem and decided to wrap a solution in a composer package.
https://github.com/TishoTM/eloquent-ci-relations
After installing the composer package, you can simply use the trait inside the eloquent models without changing anything else on the relation methods within the model.
use \TishoTM\Eloquent\Concerns\HasCiRelationships;

Laravel's Model::create() function isn't setting a custom field value

Laravel newbie here, sorry if this is painfully obvious but I've been stuck on it for ages!
Objective: To mass-assign a Quote::create() database insertion with the full values from the form, plus set the User ID to the currently logged in user.
Problem: The user_id column is never written to the database. Every other column is, but user_id remains as 0.
I have of course tried adding user_id to the $fillable array, but I don't want it to be user-fillable - I want it to be set by Laravel's Auth::id() function.
Any ideas why this won't get stored? Is it because the $quote->create() function doesn't factor in previously set data and just takes its parameter as everything to be saved? If so how do I do this?
Here's my controller's store() function:
/**
* Stores a created quote in the database
*
* #param QuoteRequest $request
*
*/
public function store(QuoteRequest $request)
{
// This method will only get fired if QuoteRequest passes
$quote = new Quote;
$quote->user_id = Auth::id();
$quote->create($request->all());
echo 'Job done';
}
Here's my Quote model:
<?php namespace App;
use Illuminate\Database\Eloquent\Model;
use Auth;
class Quote extends Model {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'quotes';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'quote_person',
'quote_value',
'quote_date'
];
/**
* The attributes excluded from the model's JSON form.
*
* #var array
*/
protected $hidden = [ ];
/*
* Request/User many-to-one relationship
*/
public function user()
{
return $this->belongsTo('App\User');
}
/*
* Belongs to current User scope
*/
public function scopeMine($query)
{
return $query->where('user_id', Auth::id());
}
}
Try this and see if it works.
public function store(QuoteRequest $request)
{
// This method will only get fired if QuoteRequest passes
$quote = new Quote;
$quote->fill($request->all());
$quote->user_id = Auth::id();
$quote->save();
echo 'Job done';
}
The create function is considered mass assignment and is thusly affected by $fillable / $guarded. It's also a static function, so $quote->create() is making an entirely new Eloquent instance - that's why your manually assigned user_id is lost.
You can use Model::unguard() to temporarily turn off the protection.

Categories