Model trait causing SQL error during insert - php

I have an app model that uses a trait. I need the trait to receive some data and using it in the accessor function.
Content model:
use App\Traits\ArchiveTrait;
class Content extends Model
{
use ArchiveTrait;
protected $fillable = ['title','details'];
protected $table = 'contents';
public function getFileName($file_name)
{
return $this->archiving->url.'/media/contents/'.$file_name;
}
}
Archive Trait
trait ArchiveTrait {
private $app_id;
private $archiving;
public function __construct()
{
$this->app_id = config('archiving.id');
$this->archiving = Application::findOrFail($this->app_id);
}
public function guzzleClient() {
$headers = [
'Content-Type' => 'application/json',
'X-Requested-With' => 'XMLHttpRequest',
'Authorization' => 'Bearer ' . $this->archiving->token,
];
$client = new \GuzzleHttp\Client([
'headers' => $headers,
'http_errors' => false
]);
return $client;
}
}
Then problem is, when I try to insert a new content, I get the 'Field 'title' and 'details' doesn't have a default value' SQL error but if I remove the 'use ArchiveTrait' and the getFileName function from the model, then it works just fine. I think there's something wrong with the trait.

Laravel Eloquent models get their attributes passed through via the constructor. Because you are overriding the constructor and not passing anything to the parent constructor, the attributes of your model are never set.
You shouldn't add a constructor in a trait, but if you really needed to, you could fix this by passing the $attributes variable to the parent constructor:
public function __construct($attributes = [])
{
parent::__construct($attributes);
$this->app_id = config('archiving.id');
$this->archiving = Application::findOrFail($this->app_id);
}

Related

Laravel Custom Model Class - Field Does not have a Default Value

So, I have a custom Model extension class called RecursiveModel:
use Illuminate\Database\Eloquent\Model;
use ... RecursiveHelper;
class RecursiveModel extends Model {
private $recursiveHelper = null;
public function __construct(){
$this->recursiveHelper = new RecursiveHelper();
parent::__construct();
}
public function save(array $options = []){
parent::save($options);
}
...
// Additional methods available for Recursive Models (self-referenced `parent_id` relationships)
}
And, a Model that extends this RecursiveModel class instead of the base Model class:
use ... RecursiveModel;
use Illuminate\Database\Eloquent\SoftDeletes;
class Line extends RecursiveModel {
use SoftDeletes;
protected $table = "lines";
protected $primaryKey = "id";
public function parent(){
return $this->belongsTo(self::class, "parent_id", "id");
}
public function children(){
return $this->hasMany(self::class, "parent_id", "id");
}
}
All is well and good, and with previously imported records (back when Line extended Model and not RecursiveModel, I was able to use my RecursiveHelper methods/logic without issue. Now, I'm trying to refresh my database, which calls a Seeder:
use Illuminate\Database\Seeder;
use ... Slugger;
use ... Line;
class LinesSeeder extends Seeder {
public function run(){
$parentLine = Line::create([
"name" => "Line Item",
"slug" => $this->slugger->slugify("Line Item"),
"created_at" => date("Y-m-d H:i:s"),
"updated_at" => date("Y-m-d H:i:s"),
]);
$childLine = Line::create([
"name" => "Child Line Item",
"slug" => $this->slugger->slugify("Child Line Item"),
"parent_id" => $parentLine->id,
"created_at" => date("Y-m-d H:i:s"),
"updated_at" => date("Y-m-d H:i:s"),
]);
...
}
}
As previously stated, when Line extended Model and not RecursiveModel, this code worked without issue. But now, I'm running into this error:
SQLSTATE[HY000]: General error: 1364 Field 'name' doesn't have a default value (SQL: insert into lines
(updated_at, created_at) values (2018-08-13 15:56:45, 2018-08-13 15:56:45))
The Line::create([...]); doesn't seem to be receiving the parameter passed; is there something I'm missing when extending Model.php? I've tried adding:
public function create(array $options = []){
parent::create($options);
}
To RecursiveModel, but that just throws another error (and I don't think the create() method is a part of Model.php, but rather Builder.php.)
Also, it's not an issue with protected $fillable, nor is it an issue with setting 'strict' => true, on my mysql connection; already tried both of those to no avail.
As suggested, updated __construct method of RecursiveModel to:
public function __construct(array $attributes = []){
$this->recursiveHelper = new RecursiveHelper();
return parent::__construct($attributes);
}
Unfortunately, still getting the same error.
Edit: Line.php had a __construct method that was carried over from when I was applying $this->recursiveHelper model by model; solution was to update signature to match (as noted above) or remove __construct from extending models.
Model constructors need to take in an array of attributes:
public function __construct(array $attributes = [])

Mockery, App Instance not working on some laravel tests

Basically, my question is; Why does the $this->app->instance() Call work on one instance of the mocked object, but the other doesn't...
In the example below, the getGroupSingleSignOnLink function actually gets called, the other is mocked and the test passes...
TEST
namespace Tests\Feature;
use App\Models\Group;
use App\Models\User;
use Tests\TestCase;
use App\Clients\SingleSignOnApi;
use Mockery;
class SingleSignOnTest extends TestCase
{
private $validUrl = 'http://www.google.com';
public function setUp()
{
parent::setUp();
$single_sign_on = Mockery::mock(SingleSignOnApi::class);
$single_sign_on->shouldReceive('getGroupSingleSignOnLink')->andReturn($this->validUrl);
$single_sign_on->shouldReceive('getSingleSignOnLink')->andReturn($this->validUrl);
$this->app->instance(SingleSignOnApi::class, $single_sign_on);
}
//THIS TEST FAILS, SingleSignOnApi Class Not Mocked
public function testGroupAuthConnection()
{
$group = Group::whereNotNull('external_platform_key')->first();
$user = $group->users()->first();
$this->be($user);
$group_sso = $group->groupAuthConnections()->first();
$response = $this->get(route('sso.group.connect', ['id' => $group_sso->id]));
$response->assertRedirect($this->validUrl);
$response->assertSessionMissing('__danger');
}
//THIS TEST PASSES, The SingleSignOnApi Class is Mocked
public function testAuthConnectionConnect()
{
$user = User::first();
$this->be($user);
$sso = $user->authConnections()->firstOrFail();
$response = $this->get(route('sso.connect', ['id' => $sso->id]));
$response->assertRedirect($this->validUrl);
$response->assertSessionMissing('__danger');
}
}
CONTROLLER FUNC - TEST MOCK WORKING
public function connect($id)
{
$auth_connection = $this->findAuthConnection($id, Auth::user());
$sso_client = App::make(SingleSignOnApi::class);
$url = $sso_client->getSingleSignOnLink($auth_connection);
return redirect($url);
}
CONTROLLER FUNC - TEST MOCK NOT WORKING
public function connect($id)
{
$group_ids = Auth::user()->groups()->pluck('groups.id')->toArray();
$group_auth_connection = $this->findGroupAuthConnection($id, Auth::user());
//This is the Mocked Object in my Test: SingleSignOnApi
$sso_client = App::make(SingleSignOnApi::class, [$group_auth_connection->group->external_platform_key]);
$url = $sso_client->getGroupSingleSignOnLink($group_auth_connection, Auth::user());
return redirect($url);
}
I'll use Quickbooks as an example to illustrate how I got this to work consistently for me.
Here's my AppServiceProvider, defining a custom QuickbooksSDK Class I created:
...
public function boot()
{
$this->app->bind(QuickbooksSDK::class, function($app) {
return new QuickbooksSDK(
new GuzzleHttp\Client([
'base_uri' => config('invoicing.baseUri'),
'timeout' => 3,
'headers' => [
'Authorization' => 'Bearer '.config('invoicing.apiKey'),
'Accept' => 'application/json'
],
'http_errors' => false
]),
config('invoicing.apiKey'),
env('QUICKBOOKS_REFRESH_TOKEN'),
env('QUICKBOOKS_CLIENT_ID'),
env('QUICKBOOKS_CLIENT_SECRET')
);
});
}
Then I created a second custom class, called the QuickbooksInvoicingDriver, that takes the instantiated SDK Class from the Service container:
public function __construct()
{
$this->api = app(QuickbooksSDK::class);
}
Finally, in my test class, I can mock the QuickbooksSDK, with my own custom responses, to make testing easier:
$vendorResponse = '{"Vendor": {"Balance": 0,"Vendor1099": false,"CurrencyRef": {"value": "GYD","name": "Guyana Dollar"},"domain": "QBO","sparse": false,"Id": "25","SyncToken": "0","MetaData": {"CreateTime": "2018-04-04T12:36:47-07:00","LastUpdatedTime": "2018-04-04T12:36:47-07:00"},"DisplayName": "daraul","PrintOnCheckName": "daraul","Active": true},"time": "2018-04-04T12:36:47.576-07:00"}';
$mock = new MockHandler([
new Response(200, [], $vendorResponse),
]);
$handler = HandlerStack::create($mock);
$client = new Client(['handler' => $handler]);
$api = new QuickbooksSDK(
$client,
'test',
'test',
'test',
'test'
);
$this->app->instance(QuickbooksSDK::class, $api);
Now I can run my tests normally, without worrying about third parties. These links were really helpful for me:
http://docs.guzzlephp.org/en/stable/testing.html
https://laravel.com/docs/5.5/container
Let me know if this was helpful.

Send through object reference from Laravel Job (simple OOP?)

I'm trying to pass a newed up Model Object as a reference from a Laravel Job to the called Object and method. I need to new up the particular model in the Job because I need it in the Job's failed() method to update that model (and persist to database) as it functions as a log.
class ScrapeJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $store;
protected $scrape;
public function __construct(Store $store)
{
$this->store = $store;
}
public function handle()
{
$this->scrape = new \App\Scrape; // This is the log object.
// Here I call the SuperStoreScraper
$class = '\App\Scraper\\' . $this->store->scraper;
(new $class($this->store, $this->scrape))->scrape();
}
public function failed(\Exception $e)
{
// Update the Scrape object and persist to database.
$data = $this->scrape->data; // here I get the error... ->data is not found?
$data['exception'] = [ // this should work since I'm casting data to an array in the Model class.
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
];
$this->scrape->data = $data;
$this->scrape->status = 'failed';
$this->scrape->save();
}
}
class SuperStoreScraper extends Scraper
{
public function __construct(Store $store, Scrape &$scrape) {
parent::__construct($store, $scrape);
}
public function scrape() {
$this->start();
}
}
abstract class Scraper
{
protected $store;
protected $scrape;
public function __construct(Store $store, Scrape &$scrape) {
$this->store = $store;
$this->scrape = &$scrape;
}
protected function start()
{
$this->scrape->fill([
'data' => [
'store' => $this->store->name,
'scraper' => $this->store->scraper
],
'status' => 'scraping'
])->save();
}
}
Everything seems to work fine. The newed up object is passed to both the SuperStoreScraper and the parent Scraper class (through the constructor), BUT when I persist it to the database in the Scraper object start() method, it doesn't reflect that up to the ScrapeJob (which newed up the Scrape object) and then I get an error in the Job's failed() method when trying to update the persisted object.
ErrorException: Trying to get property of non-object in ...\app\Jobs\ScrapeJob.php:54
What am I missing here?
I think I have kind of solved my issue now.
I've moved the \App\Scrape instantiation to the Job's __constructor, but also persist it to the database, like this:
protected $scrape;
public function __construct(Store $store)
{
$this->store = $store;
$this->scrape = \App\Scrape::create(['status' => 'queued']);
}
This works, I can access the Scrape model from both the Scraper and the Job's failed() method, BUT, if I have multiple tries on the Job I'd like to create new Scrape instantiations for each try. Right now the same Scrape instance (same id) is just updated with each Job retry.

Extend/override Eloquent create method - Cannot make static method non static

I'm overriding the create() Eloquent method, but when I try to call it I get Cannot make static method Illuminate\\Database\\Eloquent\\Model::create() non static in class MyModel.
I call the create() method like this:
$f = new MyModel();
$f->create([
'post_type_id' => 1,
'to_user_id' => Input::get('toUser'),
'from_user_id' => 10,
'message' => Input::get('message')
]);
And in the MyModel class I have this:
public function create($data) {
if (!Namespace\Auth::isAuthed())
throw new Exception("You can not create a post as a guest.");
parent::create($data);
}
Why doesn't this work? What should I change to make it work?
As the error says: The method Illuminate\Database\Eloquent\Model::create() is static and cannot be overridden as non-static.
So implement it as
class MyModel extends Model
{
public static function create($data)
{
// ....
}
}
and call it by MyModel::create([...]);
You may also rethink if the auth-check-logic is really part of the Model or better moving it to the Controller or Routing part.
UPDATE
This approach does not work from version 5.4.* onwards, instead follow this answer.
public static function create(array $attributes = [])
{
$model = static::query()->create($attributes);
// ...
return $model;
}
Probably because you are overriding it and in the parent class it is defined as static.
Try adding the word static in your function definition:
public static function create($data)
{
if (!Namespace\Auth::isAuthed())
throw new Exception("You can not create a post as a guest.");
return parent::create($data);
}
Of course you will also need to invoke it in a static manner:
$f = MyModel::create([
'post_type_id' => 1,
'to_user_id' => Input::get('toUser'),
'from_user_id' => 10,
'message' => Input::get('message')
]);

How to set a default attribute value for a Laravel / Eloquent model?

If I try declaring a property, like this:
public $quantity = 9;
...it doesn't work, because it is not considered an "attribute", but merely a property of the model class. Not only this, but also I am blocking access to the actually real and existent "quantity" attribute.
What should I do, then?
An update to this...
#j-bruni submitted a proposal and Laravel 4.0.x is now supporting using the following:
protected $attributes = array(
'subject' => 'A Post'
);
Which will automatically set your attribute subject to A Post when you construct. You do not need to use the custom constructor he has mentioned in his answer.
However, if you do end up using the constructor like he has (which I needed to do in order to use Carbon::now()) be careful that $this->setRawAttributes() will override whatever you have set using the $attributes array above. For example:
protected $attributes = array(
'subject' => 'A Post'
);
public function __construct(array $attributes = array())
{
$this->setRawAttributes(array(
'end_date' => Carbon::now()->addDays(10)
), true);
parent::__construct($attributes);
}
// Values after calling `new ModelName`
$model->subject; // null
$model->end_date; // Carbon date object
// To fix, be sure to `array_merge` previous values
public function __construct(array $attributes = array())
{
$this->setRawAttributes(array_merge($this->attributes, array(
'end_date' => Carbon::now()->addDays(10)
)), true);
parent::__construct($attributes);
}
See the Github thread for more info.
This is what I'm doing now:
protected $defaults = array(
'quantity' => 9,
);
public function __construct(array $attributes = array())
{
$this->setRawAttributes($this->defaults, true);
parent::__construct($attributes);
}
I will suggest this as a PR so we don't need to declare this constructor at every Model, and can easily apply by simply declaring the $defaults array in our models...
UPDATE:
As pointed by cmfolio, the actual ANSWER is quite simple:
Just override the $attributes property! Like this:
protected $attributes = array(
'quantity' => 9,
);
The issue was discussed here.
I know this is really old, but I just had this issue and was able to resolve this using this site.
Add this code to your model
protected static function boot()
{
parent::boot();
static::creating(function ($model) {
$model->user_id = auth()->id();
});
}
Update/Disclaimer
This code works, but it will override the regular Eloquent Model creating Event
I use this for Laravel 8 (static and to dynamically change attributes)
<?php
namespace App\Models\Api;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
/**
* Indicates if the model should be timestamped.
*
* #var bool
*/
public $timestamps = false;
protected static function defAttr($messages, $attribute){
if(isset($messages[$attribute])){
return $messages[$attribute];
}
$attributes = [
"password" => "123",
"created_at" => gmdate("Y-m-d H:i:s"),
];
return $attributes[$attribute];
}
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::creating(function ($messages) {
$messages->password = self::defAttr($messages, "password");
$messages->created_at = self::defAttr($messages, "created_at");
});
}
}
Set the attribute value in the constructor:
public function __construct()
{
$this->attributes['locale'] = App::currentLocale();
}

Categories