Skip to content
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

Merged
merged 9 commits into from
Oct 1, 2024

Conversation

Vitaliy-1
Copy link
Collaborator

No description provided.

&& (
$announcement->getDateExpire() == null || strtotime($announcement->getDateExpire()) > time()
$announcement->getAttribute('dateExpire') == null || strtotime($announcement->getAttribute('dateExpire')) > time()
Copy link
Member

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.

Copy link
Member

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 .

Copy link
Member

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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Like this!

@Vitaliy-1 Vitaliy-1 force-pushed the i10328_eloquent_announcements branch 2 times, most recently from d6dbb9f to 5253484 Compare September 16, 2024 09:57

// 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]
Copy link
Collaborator Author

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?
Copy link
Collaborator Author

@Vitaliy-1 Vitaliy-1 Sep 16, 2024

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?

Copy link
Contributor

@Hafsa-Naeem Hafsa-Naeem Sep 23, 2024

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened an issue: #10487

}

// TODO What should the default behaviour be if localized value doesn't exist?
return $multilingualProp[$locale] ?? null;
Copy link
Collaborator Author

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();
Copy link
Collaborator Author

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

Copy link
Member

@touhidurabir touhidurabir left a 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.

api/v1/announcements/PKPAnnouncementController.php Outdated Show resolved Hide resolved
$sendEmail = (bool) filter_var($params['sendEmail'], FILTER_VALIDATE_BOOLEAN);

if ($context) {
$this->notifyUsers($request, $context, $announcementId, $sendEmail);
$this->notifyUsers($request, $context, $announcement->getKey(), $sendEmail);
Copy link
Member

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 .

Copy link
Collaborator Author

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
},

api/v1/announcements/PKPAnnouncementController.php Outdated Show resolved Hide resolved
{
$this->setData('assocType', $assocType);
}
protected $fillable = ['assocType', 'assocId', 'typeId', 'title', 'image', 'description', 'descriptionShort'];
Copy link
Member

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'];

Copy link
Member

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);
    }

classes/announcement/Announcement.php Outdated Show resolved Hide resolved
classes/core/traits/ModelWithSettings.php Outdated Show resolved Hide resolved
classes/core/traits/ModelWithSettings.php Outdated Show resolved Hide resolved
classes/core/traits/ModelWithSettings.php Outdated Show resolved Hide resolved
&& (
$announcement->getDateExpire() == null || strtotime($announcement->getDateExpire()) > time()
$announcement->getAttribute('dateExpire') == null || strtotime($announcement->getAttribute('dateExpire')) > time()
Copy link
Member

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 .

@Vitaliy-1 Vitaliy-1 merged commit ffbc59a into pkp:main Oct 1, 2024
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants