Inconsistent behaviour when using enums in where query - php

I'm working on a website for a simracing hotlapping competition. Participants can submit an unlimited amount of lap times for a specific round in a specific season, and admins can either approve or deny these lap times. The fastest approved lap time for each participant is shown on that round's leaderboard.
In case it's relevant, I'm using this package to generate Snowflake IDs instead of incremental IDs or GUIDs.
I'm using a pretty simple enum for the lap time status;
enum LapTimeStatus: int
{
case SUBMITTED = 0;
case APPROVED = 1;
case DENIED = 2;
}
In the LapTime model I cast this field to the enum, and I've of course added the relationship to the Round model;
class LapTime extends Model
{
use HasFactory, Snowflake;
protected $casts = [
'status' => LapTimeStatus::class,
];
public function round(): BelongsTo
{
return $this->belongsTo(Round::class);
}
}
In the Round model, I've set up the inverse of the relationship, as well as a method to fetch the lap times for the aforementioned leaderboard;
class Round extends Model
{
use HasFactory, Snowflake;
public function times(): HasMany
{
return $this->hasMany(LapTime::class);
}
public function timesForLeaderboard(): array
{
$times = $this->times()
->with('user')
->orderBy('lap_time')
->where('status', LapTimeStatus::APPROVED)
->get();
return $times->unique('user_id')->values()->toArray();
}
}
timesForLeaderboard gets all approved times for that round, orders them by laptime (laptimes are stored in milliseconds for easy sorting), and makes sure only one lap time for each user is selected.
However, the ->where('status', LapTimeStatus::APPROVED) line is causing issues. It works perfectly fine for the first season, however for whatever reason this line returns 0 models for each round, despite all of them having at least one approved lap time. I've been able to fix it by changing the line to ->where('status', (string) LapTimeStatus::APPROVED->value), but
this seems like a band aid fix for a deeper issue, as it behaves correctly elsewhere and
I'd have to make this change in a few other places, which I'd like to avoid.
Any idea what could be causing this inconsistent behaviour despite near-identical circumstances?
As requested, here's the DB schema
And as request as well, the LapTime table migration;
return new class extends Migration {
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('lap_times', function (Blueprint $table) {
$table->unsignedBigInteger('id')->primary();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('round_id')->constrained()->cascadeOnDelete();
$table->unsignedBigInteger('lap_time');
$table->string('video_url');
$table->unsignedInteger('status')->default(LapTimeStatus::SUBMITTED->value);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('lap_times');
}
};

Related

Eloquent model is returning as builder

I'm relearning Laravel with laravel 7 and have hit an issue where I'm unable to query a record in my database table. So instead of a call like $test = Test::find_by_id_and_name(1, 'test 1'); (and also $test = Test::where('id', 1); returning a class of Illuninate\Database\Eloquent\Model it returns a class of Illuminate\Database\Eloquent\Builder.
I have created a Migration for a table called Tests and seeded it with a few rows of test data. The Test Model in App is as follows
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Test extends Model
{
protected $guarded = [];
use SoftDeletes;
}
the Migration is:
se Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateTestsTable extends Migration
{
/**
* Run the migrations.
*
* #return void
*/
public function up()
{
Schema::create('tests', function (Blueprint $table) {
$table->id();
$table->string( 'name' );
$table->string( 'url', 255 );
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* #return void
*/
public function down()
{
Schema::dropIfExists('tests');
}
}
So anyone any idea why I'm not getting the Model I need so i can do for instance a dd($test); and see the values stored in the database for the row with the id of 1? or even do an echo($test->name); and see the name of this item?
thanks
* ADDITIONAL *
Should of pointed out my initial code had Test::find_by_id_and_name(1, 'test 1'); but this didn't work and throw an exception about finding the class. I modified if with where and above was a typo as it was where( 'id', 1 ); (I've corrected the code using my initial find_by code). Adding a get() or any other thing now returns null. I have verified that the database contains the table tests and that an item with the id and name of 'test 1' exists
* RESULT *
The underlying issue in the end was the data, the url had https::// in it (additional colon) so indeed it would return null. Thanks guys helped me find the reason.
Misunderstanding of query builder vs models in Laravel. Check doc for reference.
Calling query builder method statically on a model returns a builder.
User::where('id', 1); // returns builder
To resolve a query builder you can either use get() or first().
User::where('id', 1)->get(); // Returns a collection of users with 1 element.
User::where('id', 1)->first(); // Returns one user.
You can also fetch the user out of the collections this is not recommended as you might as well just call first().
User::where('id', 1)->get()->first(); // Returns collection fetches first element that is an user.
Laravel has static methods for finding models by id.
User::find(1); // returns user or null
User::findOrFail(1); // returns user or exception
Try to use the following
$test = Test::find(1);
Then you will get the record,

How to copy datas from one table into another one with all columns but not id in Laravel?

I use Laravel 6.x and I need to copy datas from one table to another. As usual both of tables has many columns and I'm looking for a solution where I don't depend on column names. In the future maybe columns will be change and I don't want to touch this part of software on every changes.
I want to do something like this:
INSERT INTO product_copys (SELECT * from products);
I want to copy all columns without the id from the products table.
I use Product and ProductCopy models to handle these datas.
Is there any handy solution for this in Laravel, Eloquent?
You may use the following
In your App\Product Model
class Product extends Model
{
protected $hidden = ['id'];
//...
}
Then in your Controller
$copy = Product::all()->toArray();
ProductCopy::insert($copy);
If you need to process a lot (thousands) of Eloquent records, using
the chunk command will allow you to do without eating all of your RAM:
Product::chunk(200, function($products)
{
ProductCopy::insert($products->toArray());
});
https://laravel.com/docs/7.x/eloquent#chunking-results
I found a solution to get columns without id column (thanks to Laravel Tricks). Here is code for the Product model:
/**
* Get all columns of model
*
* #return Array
*/
public static function getTableColumns() {
$model = new Product();
return $model->getConnection()
->getSchemaBuilder()
->getColumnListing($model->getTable());
}
/**
* Return model's columns without given columns
*
* #param Array $without_columns
* #return Array
*/
public static function withoutColumn(Array $without_columns) {
return array_diff(self::getTableColumns(), $without_columns);
}
And here you can use it:
$columns = Product::withoutColumn(['id']);
Now need only run a raw SQL query in the controller:
public function backup() {
$columns = implode(',', Product::withoutColumn(['id']));
DB::statement('INSERT INTO product_copys ('.$columns.', date_of_backup) (SELECT ' .
$columns .', NOW() AS date_of_backup FROM products)');
return Response::HTTP_OK;
}

How to delete single (many-) rows from one-to-many relations in Laravel 5.5

I got two models in Laravel: Label
class Label extends \Eloquent
{
protected $fillable = [
'channel_id',
'name',
'color',
];
public function channel()
{
return $this->belongsTo('App\Channel');
}
}
And Channel
class Channel extends \Eloquent
{
protected $fillable = [
'name',
];
public function labels()
{
return $this->hasMany('App\Label');
}
}
Now when a label is deleted, I want to make sure, the label belongs to the channel.
This works pretty good, as it even is atomic, so the row will just be deleted, if the label really belongs to the channel.
LabelController:
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function destroy($id)
{
$channel = $this->getChannel();
Label::where('id', $id)
->where('channel_id', $channel->id)
->delete();
return back();
}
And my question is now: How to build that with Eloquent, so it is elegant? Something like:
$channel->labels()->destroy($id);
But there is no destroy function on the relation.
Update:
I managed to achieve something in the right direction:
$channel->labels()->find($id)->delete();
This deletes the label with $id BUT just if the label has the right channel_id assigned. If not, I get the following error, which I could catch and handle:
FatalThrowableError (E_ERROR) Call to a member function delete() on null
Still, as Apache is threaded, there could be the case that another thread changes the channel_id after I read it. So the only way besides my query is to run a transaction?
If you want to delete related items of your model but not model itself you can do like this:
$channel->labels()->delete(); It will delete all labels related to the channel.
If you want to delete just some of labels you can use where()
$channel->labels()->where('id',1)->delete();
Also if your relation is many to many it will delete from third table too.
You mentioned that, you first want to check if a label has a channel. If so, then it should be deleted.
You can try something like this though:
$label = Label::find($id);
if ($label->has('channel')) {
$label->delete();
}
You can use findorfail:
$channel->labels()->findOrFail($id)->delete();

Populating parameters with default value using relationships in Laravel 5.2

Okay, so I have a question. I'm programming a really complex report and the interface uses Laravel 5.2. Now the thing is that, depending on certain conditions, the user does not always need all parameters to be filled. However, for simplicity purposes, I made it so that the report always receives the complete set of parameters no matter what. So I have three tables:
tblReportParam
ID
ParamName
DefaultValue
tblReportParamValue
ParamID
ReportID
Value
tblReport
ID
UserName
Now, I have a solution that works, but for some reason, it just feels like I should be able to make better use of models and relationships. I basically have just my models and controllers and solved the whole thing using SQL.
It feels somewhat close to this but not quite. So basically, you need to always load/save all parameters. If parameter x is actually defined by the user then you use his definition otherwise you go with the default defined in tblReportParam. Anyone has any idea how to do this?
EDIT:
Okay, so I checked Eddy's answer and tried to work it in our system, but another colleague of mine started implementing a many-to-many relationship between the tblReport and the tblReportParam table with the tblReportParamValue acting as the pivot so I'm having some difficulty adapting this solution for our system. Here's the two models:
class ReportParam extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'tblReportParam';
protected $primaryKey = 'ID';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['ID', 'NomParam', 'DefaultValue'];
public function renourapports()
{
return $this->belongsToMany('App\Report');
}
}
class Report extends Model
{
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'tblReport';
protected $primaryKey = 'ID';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = ['ID', 'NoEmploye', 'NoClient', 'NoPolice', 'DateCreation', 'DateModification', 'runable', 'DernierEditeur'];
public $timestamps = false;
public function params()
{
return $this->belongsToMany('App\ReportParam ', 'tblReportParamValue', 'ReportID', 'ParamID')->withPivot('Valeur');
}
}
Now this actually is a pretty neat solution, but it only works if the parameter is actually in the pivot table (i.e. the relationship actually exists). What we want is that for the parameters that aren't in the pivot table, we simply want their default value. Can Eddy's solution work in this case?
Using Eloquent models
class ReportParam extends Model
{
public function paramValue() {
return $this->hasOne('App\ReportParamValue', 'ParamID');
}
public function getDefaultValueAttribute($value) {
if ( $this->paramValue ) return $this->paramValue->Value; //relationship exists
return $this->DefaultValue;
}
}
$reportParam->value; // return the relationship value or the default value;
UPDATE
Now that tblReportParamValue is a pivot table you should redefine your relationships. In ReportParam model add
public function reports() {
return $this->belongsToMany('App\Report', 'tblReportParamValue', 'ParamID', 'ReportID')->withPivot('Value');
}
And in Report model, defined the opposite
public function params() {
return $this->belongsToMany('App\ReportParam', 'tblReportParamValue', 'ReportID', 'ParamID')->withPivot('Value');
}
Now getting the default value from ReportParam becomes too complicated because it will one ReportParam has Many Reports. So doing $reportParam->reports() will bring back every single report that uses that paramID in the pivot table. Therefore looking for a value would mean going through all the reports. We could avoid that by changind the function definition.
public function getDefaultValue($reportID) {
$reportValue = $this->reports()->wherePivot('ReportID', $reportID)->first();
return $reportValue ? $this->reportValue->Value : $this->DefaultValue;
}
//In Controller
$report = Report::find(1);
$reportParam = ReportParam::find(1);
$reportParam->getDefaultValue($report->ID);
Ok I think this might work. If it doesnt, I am really sorry, I don't know any better.

Laravel - many-to-many where the many-to-many table is (part-) polymorph

I have a table called bonus. A user can get a bonus (it's like an reward) for certain actions. Well, the bonus can be assigned to many users and many users can get the same bonus. So it's a many to many relation between user and bonus.
This is no problem so far. But users can get the same bonus for different actions. So let's say there is a bonus for voting on a picture. Well, one user could vote on one picture and another one could vote on another picture which I'd like to save in the many-to-many table.
Furthermore there could be a bonus for writing a comment which is clearly another table than picture votes.
The problem here is that I would need to save the polymorphic type in the bonus table and the ID in the many-to-many table.
I think this should be the best way but how would I realize it with laravel? I think this is not a normal use case. But still I'd like to use it as other relations in laravel so that I could fetch a user and get his bonuses with the correct polymorphic relation.
Do you have any ideas?
You are probably going to have to develop your own relationship classes.
Ex:
MODEL
public function answers()
{
$instance = new Response();
$instance->setSid($this->sid);
return new QuestionAnswerRelation($instance->newQuery(),$this);
}
RELATIONSHIP
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
use Pivotal\Survey\Models\Answer;
use Pivotal\Survey\Models\Collections\AnswerCollection;
use Pivotal\Survey\Models\QuestionInterface;
use Pivotal\Survey\Models\SurveyInterface;
class QuestionAnswerRelation extends Relation
{
/**
* Create a new relation instance.
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #param \Illuminate\Database\Eloquent\Model $parent
* #return void
*/
public function __construct(Builder $query, QuestionInterface $parent)
{
$table = $query->getModel()->getTable();
$this->query = $query
->select(array(
\DB::raw($parent->sid.'X'.$parent->gid.'X'.$parent->qid . ' AS value'),
'id'
));
$this->query = $query;
$this->parent = $parent;
$this->related = $query->getModel();
$this->addConstraints();
}
public function addEagerConstraints(array $models)
{
parent::addEagerConstraints($models);
}
public function initRelation(array $models, $relation)
{
}
public function addConstraints()
{
}
public function match(array $models, Collection $results, $relation)
{
}
public function getResults()
{
$results = $this->query->get();
$answerCollection = new AnswerCollection();
foreach($results as $result)
{
$answer = new Answer($result->toArray());
$answer->question = $this->parent;
$answerCollection->add($answer);
}
return $answerCollection;
}
In this case we are using Lime Survey which creates a unique table (note the $instance->setSid() changes the table name) for each of its surveys and a unique column for each of its answer -> question values. ( note $parent->sid.'X'.$parent->gid.'X'.$parent->qid. 'AS value')
Where sid = survey_id, gid = group_id(I think) and qid = question_id
Its was quite irritating.
Note how I reference values from the parent to further develop the query.
You should be able to follow a similar route to achieve whatever your heart desires and still maintain the feasibility to use Eloquent.

Categories