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

Parent _lft and _rgt not updated after adding child using create() method #591

Open
ohchiko opened this issue Dec 7, 2023 · 2 comments
Open

Comments

@ohchiko
Copy link

ohchiko commented Dec 7, 2023

I implemented NodeTrait trait on an abstract class, then modified its ancestors() method to use newModelQuery() instead of newQuery() in order to retrieve ancestors without using any global scopes that is implemented on the child class. On the child class, a global scope is applied to query based on the parent_id (see below). Also, on one of the child class, a creating() model event is applied to ensure the model is always saved as root.

abstract class Category extends Model
{
    use NodeTrait;

    public function ancestors()
    {
        return new AncestorRelation($this->newModelQuery(), $this);
    }
}

class RootCategory extends Category
{
    protected static function booted(): void
    {
        static::addGlobalScope('rootScope', function (Builder $builder): void {
            $builder->whereNull('parent_id');
        });

        static::creating(function (self $rootCategory): void {
            $rootCategory->makeRoot();
        });
    }
}

class SubCategory extends Category
{
    protected static function booted(): void
    {
        static::addGlobalScope('subScope', function (Builder $builder): void {
            $builder->whereNotNull('parent_id');
        });
    }
}

How to reproduce:

$rootCategory = RootCategory::create(['name' => fake()->word()]);
// returns the created model with:
//     id: 1
//     parent_id: null
//     _lft: 1
//     _rgt: 2

$subCategory = SubCategory::create(['name' => fake()->word()], $rootCategory);
// returns the created model with:
//     id: 2
//     parent_id: 1
//     _lft: 2
//     _rgt: 3

$rootCategory->refresh();
// the _lft and _rgt is still 1 and 2, not 1 and 4 as I was expected

The _lft and _rgt of the $rootCategory on the database is also 1 and 2.

Was this expected?
I'll provide more codes if needed.
Thanks!

@ohchiko
Copy link
Author

ohchiko commented Dec 7, 2023

I found the solution for the above issue.
So, what happened is because there is global scopes applied, the newQuery() of the model will always returned a query builder with scopes applied.
Upon inserting a node into a parent, the package uses the newQuery() method (or withTrashed()), see NodeTrait.php#L674.

The solution is either override the newNestedSetQuery() method or directly override the model's newQuery() method. For this case, I override the newNestedSetQuery() method because this happens only on this package. Modifying the newQuery() method would cause any package or code that calls it changed, which is currently not what I want to achieve.

Lastly, I wonder if this is the solution or there was another built-in solution on this package in order to apply global scopes without affecting the query builder.

abstract class Category extends Model
{
    use NodeTrait;

    public function ancestors()
    {
        return new AncestorRelation($this->newModelQuery(), $this);
    }

    public function newNestedSetQuery($table = null)
    {
        $builder = $this->usesSoftDelete()
            ? $this->withTrashed()
            : $this->newQuery();

        return $this->applyNestedSetScope(($builder->withoutGlobalScopes(), $table);
    }
}

@ohchiko
Copy link
Author

ohchiko commented Dec 8, 2023

In addition to solution above, below is the current working solution.

abstract class Category extends Model
{
    use NodeTraitWithGlobalScope, SoftDeletes;
}

trait NodeTraitWithGlobalScope
{
    use NodeTrait;

    public function descendants()
    {
        return new DescendantsRelation($this->newNestedSetQuery(), $this);
    }

    public function ancestors()
    {
        return new AncestorsRelation($this->newNestedSetQuery(), $this);
    }

    public function newNestedSetQuery($table = null)
    {
        $builder = $this->usesSoftDelete()
            ? $this->withTrashed()
            : $this->newQuery();

        return $this->applyNestedSetScope($builder->withoutGlobalScopes(), $table);
    }

    public function newScopedQuery($table = null)
    {
        return $this->applyNestedSetScope($this->newNestedSetQuery(), $table);
    }
}

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

No branches or pull requests

1 participant