1: <?php
2:
3: namespace Baril\Bonsai\Relations;
4:
5: use Baril\Bonsai\Relations\Concerns\ExcludesSelf;
6: use Illuminate\Database\Eloquent\Builder;
7: use Illuminate\Database\Eloquent\Collection as EloquentCollection;
8: use Illuminate\Database\Eloquent\Relations\HasMany;
9:
10: /**
11: * @template TDeclaringModel of \Illuminate\Database\Eloquent\Model
12: *
13: * @extends \Illuminate\Database\Eloquent\Relations\HasMany<TDeclaringModel, TDeclaringModel>
14: */
15: class HasManySiblings extends HasMany
16: {
17: use ExcludesSelf;
18:
19: protected $withOrphans = false;
20:
21: /**
22: * @return $this
23: */
24: public function withOrphans()
25: {
26: $this->withOrphans = true;
27:
28: if (static::$constraints) {
29: $this->getRelationQuery()->withoutGlobalScope('withoutOrphans');
30: }
31:
32: return $this;
33: }
34:
35: /**
36: * @return $this
37: */
38: public function withoutOrphans()
39: {
40: $this->withOrphans = false;
41:
42: if (static::$constraints) {
43: $this->addWithoutOrphansGlobalScope();
44: }
45:
46: return $this;
47: }
48:
49: /**
50: * @return void
51: */
52: protected function addWithoutOrphansGlobalScope()
53: {
54: $this->getRelationQuery()->withGlobalScope(
55: 'withoutOrphans',
56: function ($query) {
57: return $query->whereNotNull($this->foreignKey);
58: }
59: );
60: }
61:
62: /** @inheritDoc */
63: public function addConstraints()
64: {
65: if (static::$constraints) {
66: $query = $this->getRelationQuery();
67:
68: $query->where($this->foreignKey, '=', $this->getParentKey());
69:
70: $this->addWithoutOrphansGlobalScope();
71: }
72: }
73:
74: /** @inheritDoc */
75: public function addEagerConstraints(array $models)
76: {
77: $this->where(function ($nestedWhere) use ($models) {
78:
79: $keys = $this->getKeys($models, $this->localKey);
80:
81: $whereIn = $this->whereInMethod($this->parent, $this->localKey);
82:
83: $nestedWhere->{$whereIn}(
84: $this->foreignKey,
85: $keys
86: );
87:
88: // At this point, the custom constraints that may have been
89: // provided when calling with() have not been applied yet,
90: // thus we can't trust $this->withOrphans. We have to include
91: // the orphans in the query. We can still exclude them when
92: // we do the matching.
93: if (in_array(null, $keys, true)) {
94: $nestedWhere->orWhereNull($this->foreignKey);
95: }
96: });
97: }
98:
99: /** @inheritDoc */
100: public function match(array $models, EloquentCollection $results, $relation)
101: {
102: $results = $results->when(! $this->withOrphans, function ($results) {
103: $foreignKey = explode('.', $this->foreignKey);
104: $foreignKey = end($foreignKey);
105: return $results->whereNotNull($foreignKey);
106: });
107:
108: return $this->excludeSelfFromMatchesIfExcluded(
109: parent::match($models, $results, $relation),
110: $relation
111: );
112: }
113:
114: /** @inheritDoc */
115: public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])
116: {
117: $query = parent::getRelationExistenceQuery($query, $parentQuery, $columns);
118:
119: if (! $this->withOrphans) {
120: return $query;
121: }
122:
123: $from = $query->getQuery()->from;
124: $segments = preg_split('/\s+as\s+/i', $from);
125: $as = end($segments);
126:
127: return $query->orWhere(function ($nestedWhere) use ($as) {
128: $nestedWhere->whereNull($this->getQualifiedParentKeyName());
129: $nestedWhere->whereNull($as . '.' . $this->getForeignKeyName());
130: });
131: }
132:
133: /**
134: * Overridden in order to allow null keys in Laravel 12.44+
135: * (see https://github.com/laravel/framework/commit/095ef3cd5bd0d32492cc918282d98d72e9c59fd5).
136: *
137: * @param mixed $attribute
138: * @return mixed
139: *
140: * @throws \InvalidArgumentException
141: */
142: protected function getDictionaryKey($attribute)
143: {
144: return (null === $attribute) ? '' : parent::getDictionaryKey($attribute);
145: }
146: }
147: