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

Using MorphToManyOfDescendants for graph relationships #112

Open
eli-s-r opened this issue Jun 7, 2022 · 6 comments
Open

Using MorphToManyOfDescendants for graph relationships #112

eli-s-r opened this issue Jun 7, 2022 · 6 comments
Labels
enhancement New feature or request

Comments

@eli-s-r
Copy link

eli-s-r commented Jun 7, 2022

Hi @staudenmeir, thank you so much for creating this amazing package and for releasing the new graphs feature last week, which perfectly aligns with our use-case. I was wondering if there's a way to get the functionality of the MorphtoManyOfDescendants relation that is supported for the tree structure OOTB to work for the graph structure.

Below is an oversimplified example of what I'm trying to accomplish (note also the workaround for soft-deleted pivots, there's probably a better way to handle this for the graph structure than what I'm doing?):

class Tag extends Model {
  use HasGraphRelationships {
    descendants as protected baseDescendants;
  }
  use SoftDeletes;

  public function getPivotTableName(): string {
    return 'tag_edges';
  }

  public function getParentKeyName(): string {
    return 'parent_tag_id';
  }

  public function getChildKeyName(): string {
    return 'tag_id';
  }

  public function descendants(
    bool $withTrashed = false,
  ): Descendants|Tag {
    $query = $this->baseDescendants();

    if (!$withTrashed) {
      $query->whereNull("{$this->getExpressionName()}.pivot_deleted_at");
    }

    return $query;
  }
}
/**
 * @property int $id
 * @property int $tag_id
 * @property string $taggable_type
 * @property int $taggable_id
 */
class TaggablePivot extends Model {
  use AsPivot;
  use SoftDeletes;
}
trait HasTags {
  public function tags(
    bool $withTrashed = false,
  ): MorphToMany|Tag {
    $query = $this
      ->morphToMany(
        related: Tag::class,
        name: 'taggable',
        table: 'taggable_pivot',
        foreignPivotKey: 'taggable_id',
        relatedPivotKey: 'tag_id',
      )
      ->withPivot(/* ... */);

    if (!$withTrashed) {
      $query->wherePivotNull('deleted_at');
    }

    return $query;
  }
}
class User extends Model {
  use HasTags;
}

Everything above works smoothly. What would be amazing is to be able to define a descendantTags relation on HasTags exactly like is possible for the tree structure:

public function descendantTags(
  bool $withTrashed = false,
): MorphToManyOfDescendants|Tag {
  $query = $this
    ->morphToManyOfDescendants(
      // ...
    )
    ->withPivot(/* ... */);

  if (!$withTrashed) {
    // some modified logic here
  }

  return $query;
}

I also tried using your eloquent-has-many-deep package, but I don't think it supports the intermediate Descendants relation (understandably).

Do you know of a way to get something like this to work? Thank you so much!

@staudenmeir
Copy link
Owner

Hi @eli-s-r,
Thanks. I'm working on the *OfDescendants relationships, but I'm not sure they would actually solve your case:

Do you need descendantTags to look like this?
User -> morph-to-many -> Tag -> recursive descendants -> Tag

@eli-s-r
Copy link
Author

eli-s-r commented Jun 7, 2022

@staudenmeir yes, exactly!

@eli-s-r
Copy link
Author

eli-s-r commented Jun 7, 2022

@staudenmeir so would this not be covered by the *OfDescendants relationships you mentioned you're working on?

@staudenmeir
Copy link
Owner

Unfortunately not, *OfDescendants relationships work the other way around:

Tag -> recursive descendants -> Tag -> morph-to-many -> User

Your case/direction is a whole new type of relationship.

How would you use a descendantTags relationship in your app? There might be workarounds.

@eli-s-r
Copy link
Author

eli-s-r commented Jun 7, 2022

@staudenmeir we have a bank of educational activities (and other things) that can be tagged by many things (e.g., topic). The structure of the tags is a graph, so breakfast might be a child of food, but also a child of morning activities. If a user searches for food, it's important that anything tagged with food's descendants is also in the results.

For the activity -> ancestor tags direction, what I'm doing is to use the results of the tags query as input into a second query:

public function _getAncestorTagsQuery(
  bool $taggablePivotsWithTrashed = false,
  bool $ancestorsWithTrashed = false,
  bool $includeSelf = false,
): Builder|Tag {
  $descendantTagIds = $this
    ->tags(withTrashed: $taggablePivotsWithTrashed)
    ->pluck('tags.id');

  $baseRelName = $includeSelf
    ? 'descendantsAndSelf'
    : 'descendants';

  $relName = $ancestorsWithTrashed
    // resolves via `resolveRelationUsing`
    ? Tag::getRelationNameWithTrashed($baseRelName)
    : $baseRelName;

  return Tag::query()
    ->whereHas(
      $relName,
      fn (Builder|Tag $query)
      => $query->whereIntegerInRaw(
        'id',
        $descendantTagIds,
      ),
    );
}

For the tags -> descendants -> activities direction, I'm doing the inverse:

public function _getDescendantsQueryForTaggableModel(
  string $modelClass,
  bool $descendantsWithTrashed = false,
  bool $taggablePivotsWithTrashed = false,
  bool $includeSelf = false,
): Builder|HasTagsInterface {
  $descendantQuery = $includeSelf
    ? $this->descendantsAndSelf(withTrashed: $descendantsWithTrashed)
    : $this->descendants(withTrashed: $descendantsWithTrashed);

  $descendantTagIds = $descendantQuery->pluck('id');

  $relName = $taggablePivotsWithTrashed
    ? $modelClass::getRelationNameWithTrashed('tags')
    : 'tags';

  return $modelClass::query()
    ->whereHas(
      $relName,
      fn (Builder|HasTagsInterface $query)
      => $query->whereIntegerInRaw(
        'tags.id',
        $descendantTagIds,
      ),
    );
}

edit: just realized my original goal on the first snippet above was inverted -- I want the ancestors of tags on a model, not the descendants. I updated the first snippet accordingly

@staudenmeir
Copy link
Owner

For the tags -> descendants -> activities direction, I'm doing the inverse:

A MorphToManyOfDescendants relationship as I described here would solve your inverse case, right (with Activity instead of User)?

Unfortunately not, *OfDescendants relationships work the other way around:
Tag -> recursive descendants -> Tag -> morph-to-many -> User

@staudenmeir staudenmeir added the enhancement New feature or request label Aug 9, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants