-
Notifications
You must be signed in to change notification settings - Fork 444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
pkp/pkp-lib#10328 Refactor Announcements #10382
base: main
Are you sure you want to change the base?
Conversation
&& ( | ||
$announcement->getDateExpire() == null || strtotime($announcement->getDateExpire()) > time() | ||
$announcement->getAttribute('dateExpire') == null || strtotime($announcement->getAttribute('dateExpire')) > time() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be possible to use Eloquent date casts to get columns to come back automagically as Carbon objects; it shouldn't be necessary/desirable to use strtotime
anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The case here is bit different here than the usual . we can have a cast define but as our types are coming from schema which will over write it in the ModelWithSettings
trait's convertSchemaToCasts
method where we are calling the mergeCasts
which merge the provided casts with pre defined ones . I think if we want to apply some casts like
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'dateExpire' => 'datetime',
'datePosted' => 'datetime',
];
}
we need to override the Illuminate\Database\Eloquent\Concerns\HasAttributes::mergeCasts
to defined how it will handle and determine the precedence of provided casts over defined ones .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about this a bit more, I think we should override the method mergeCasts
inside the trait ModelWithSettings
to allow model defined casts over write any casts coming form schema if schema available like
/**
* Merge new casts with existing casts on the model.
*
* @param array $casts
* @return $this
*/
public function mergeCasts($casts)
{
$casts = $this->ensureCastsAreStringValues($casts);
dump($this->casts);
$this->casts = array_merge($casts, $this->casts);
return $this;
}
basically we are just altering how the array_merge getting done .
As trait ModelWithSettings
will defined directly in entity model classes, it's mergeCasts
should have higher precedence of HasAttributes::mergeCasts
used in Illuminate\Database\Eloquent\Model
.
@@ -8,13 +8,15 @@ | |||
"properties": { | |||
"_href": { | |||
"type": "string", | |||
"origin": "composed", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Like this!
500c422
to
a3ed205
Compare
d6dbb9f
to
5253484
Compare
|
||
// TODO Eloquent transforms attributes to snake case, find and override instead of transforming here | ||
$settingValues = $settingValues->mapWithKeys( | ||
fn (mixed $value, string $key) => [Str::camel($key) => $value] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seams there is additional conversion of attributes between camel case and snake case happening somewhere in the Eloquent core. Not critical, can be handled later
$settingCount = DB::table($us)->whereIn($us . '.' . $primaryKey, $newQuery->select($primaryKey)) | ||
->update([$us . '.setting_value' => DB::raw($sql)]); | ||
|
||
return $count ? $count + $settingCount : $settingCount; // TODO Return the count of updated setting rows? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know what is better to return here since we updating two tables at once?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Vitaliy-1 Instead of returning a single summed count, could we return an associative array with separate counts for the primary table and the settings table?
we can also use database transaction (DB::transaction
) to ensure that both updates either succeed or fail together to maintain data integrity.
} | ||
|
||
// TODO What should the default behaviour be if localized value doesn't exist? | ||
return $multilingualProp[$locale] ?? null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Think if multilingual value doesn't exists it's better to just return null
->filterByContextIds([$context->getId()]) | ||
); | ||
// TODO is it OK to delete without listening Model's delete-associated events (not loading each Model)? | ||
Announcement::withContextIds([$context->getId])->delete(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deleting without models instantiating, it's much more performant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Vitaliy-1 added reviews . These are more like thoughts, suggestions and questions than reviews . Feel free to adopt if you feel like those fit the use cases .
BTW, I must say, this is very promising and will help us adopting eloquent more generally.
$sendEmail = (bool) filter_var($params['sendEmail'], FILTER_VALIDATE_BOOLEAN); | ||
|
||
if ($context) { | ||
$this->notifyUsers($request, $context, $announcementId, $sendEmail); | ||
$this->notifyUsers($request, $context, $announcement->getKey(), $sendEmail); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps rather than accessing the primary key as getKey
, how about setting a attribute setter/getter for primary key in the Announcement model as
protected function id(): Attribute
{
return Attribute::make(
get: fn($value, $attributes) => $attributes[$this->primaryKey] ?? null,
set: fn($value) => [$this->primaryKey => $value],
);
}
This will allow us the access the primary key as $modelInstance->id
which is easier and cleaner . Also we all are trying to adopt this approach .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, let's try. There is a possibility that it might interact with schema's id attribute, I don't see anything right now but id
is a primary attribute and the logic I've put to the new schema setting origin
defines Mutators and composed
attributes. Meaning in announcement.json
:
"id": {
"type": "integer",
"origin": "primary",
"readOnly": true,
"apiSummary": true
},
{ | ||
$this->setData('assocType', $assocType); | ||
} | ||
protected $fillable = ['assocType', 'assocId', 'typeId', 'title', 'image', 'description', 'descriptionShort']; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this may cause more issue than it will solve in future for us , specially for entities with schema maps with a JSON file . As we can add more props to schema via Hook like Hook::add('Schema::get::' . PKPSchemaService::SCHEMA_ANNOUNCEMENT, [$this, 'someFunction']);
, but as we have predefined fillable, eloquent will discard anything and everything not defined in the property $fillable
.
In fact in my experience this cause more confusion and frustration even when add new column via migration but forget the update the $fillable
. And this above cases where we can override props via scheme hooks, this more to that case .
However we need fillable also as otherwise any unwanted params will casue exception for example in PKPAnnouncementController::add
, we have params passed to create
as
array:8 [▼
"title" => array:2 [▶]
"descriptionShort" => array:2 [▶]
"description" => array:2 [▶]
"image" => null
"dateExpire" => null
"sendEmail" => null
"assocType" => 256
"assocId" => 1
]
where we have no column named sendEmail
.
How about we generate the fillable dynamically like
protected function constructFillable(): void
{
$schemaService = app()->get('schema'); /** @var PKPSchemaService $schemaService */
$primaryColumns = $schemaService->groupPropsByOrigin(static::getSchemaName())[Schema::ATTRIBUTE_ORIGIN_MAIN];
$this->fillable = array_merge($this->fillable, $primaryColumns, $this->settings);
}
and then have it from ModelWithSettings::__construct
as
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
if (static::getSchemaName()) {
$this->setSchemaData();
$this->constructFillable();
}
}
and in the models , we only defined a $guarded
as
protected $guarded = ['id'];
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that few entities don't have schema, probably best to allow the generation of fillable
not depending on the schema
. That is something like
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
if (static::getSchemaName()) {
$this->setSchemaData();
}
$this->constructFillable();
}
protected function constructFillable(): void
{
$primaryColumns = [];
if (static::getSchemaName()) {
$schemaService = app()->get('schema'); /** @var PKPSchemaService $schemaService */
$primaryColumns = $schemaService->groupPropsByOrigin(static::getSchemaName())[Schema::ATTRIBUTE_ORIGIN_MAIN];
}
$this->fillable = array_merge($this->fillable, $primaryColumns, $this->settings);
}
/** | ||
* See Illuminate\Database\Eloquent\Concerns\HasAttributes::mergeCasts() | ||
*/ | ||
abstract public function mergeCasts(array $casts); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason we are defining the mergeCasts
as abstract
here as it's already defined in the Illuminate\Database\Eloquent\Concerns\HasAttributes
which used as a trait in Illuminate\Database\Eloquent\Model
? unless this trait is being used in non eloquent entity classes, do we have any use of it here ?
public function __construct(array $attributes = []) | ||
{ | ||
parent::__construct($attributes); | ||
if ($this->getSchemaName()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see we are defining the method getSchemaName
as static
but then accessing as class instance method . I know laravel's handle it internally but perhaps better for us to stick with the general convention when we can . Or there any particular reason I am missing ?
$multilingualProp = $this->getAttribute($data); | ||
if (!$multilingualProp) { | ||
throw new Exception('Attribute ' . $data . ' doesn\'t exist in the ' . static::class . ' model'); | ||
} | ||
|
||
if (!in_array($data, $this->multilingualProps)) { | ||
throw new Exception('Trying to retrieve localized data from a non-multilingual attribute ' . $data); | ||
} | ||
|
||
// TODO What should the default behaviour be if localized value doesn't exist? | ||
return $multilingualProp[$locale] ?? null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be for a model instance, for some unforeseen reason, it may not contains localized data even when the localized property still defines for the model/schema . However checking via the getAttribute
, we will only get what right now set for database retrieved model instance and which may not contain any details of localized property. may be better to check if the multilingual props actually exists on model/schema and make decision based on that aviability like
if (!in_array($data, $this->getMultilingualProps())) {
throw new Exception(
sprintf("Given localizeble property %s does not exists in %s model", $data, static::class)
);
}
$multilingualProp = $this->getAttribute($data);
// TODO What should the default behaviour be if localized value doesn't exist?
return $multilingualProp[$locale] ?? null;
classes/notification/managerDelegate/AnnouncementNotificationManager.php
Show resolved
Hide resolved
&& ( | ||
$announcement->getDateExpire() == null || strtotime($announcement->getDateExpire()) > time() | ||
$announcement->getAttribute('dateExpire') == null || strtotime($announcement->getAttribute('dateExpire')) > time() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The case here is bit different here than the usual . we can have a cast define but as our types are coming from schema which will over write it in the ModelWithSettings
trait's convertSchemaToCasts
method where we are calling the mergeCasts
which merge the provided casts with pre defined ones . I think if we want to apply some casts like
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'dateExpire' => 'datetime',
'datePosted' => 'datetime',
];
}
we need to override the Illuminate\Database\Eloquent\Concerns\HasAttributes::mergeCasts
to defined how it will handle and determine the precedence of provided casts over defined ones .
No description provided.