Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ composer.lock
.phpunit.result.cache
!tests/Foundation/fixtures/hyperf1/composer.lock
tests/Http/fixtures
.env
2 changes: 2 additions & 0 deletions src/core/src/Database/Eloquent/Relations/BelongsToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Closure;
use Hyperf\Database\Model\Relations\BelongsToMany as BaseBelongsToMany;
use Hypervel\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable;
use Hypervel\Database\Eloquent\Relations\Concerns\WithoutAddConstraints;
use Hypervel\Database\Eloquent\Relations\Contracts\Relation as RelationContract;

Expand Down Expand Up @@ -44,6 +45,7 @@
*/
class BelongsToMany extends BaseBelongsToMany implements RelationContract
{
use InteractsWithPivotTable;
use WithoutAddConstraints;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
<?php

declare(strict_types=1);

namespace Hypervel\Database\Eloquent\Relations\Concerns;

use Hyperf\Collection\Collection;
use Hyperf\Database\Model\Relations\Pivot;

/**
* Overrides Hyperf's InteractsWithPivotTable to support pivot model events.
*
* When a custom pivot class is specified via `->using()`, operations like
* attach/detach/update use model methods (save/delete) instead of raw queries,
* enabling model events (creating, created, deleting, deleted, etc.) to fire.
*
* Without `->using()`, the parent's performant bulk query behavior is preserved.
*/
trait InteractsWithPivotTable
{
/**
* Attach a model to the parent.
*
* @param mixed $id
* @param bool $touch
*/
public function attach($id, array $attributes = [], $touch = true)
{
if ($this->using) {
$this->attachUsingCustomClass($id, $attributes);
} else {
parent::attach($id, $attributes, $touch);

return;
}

if ($touch) {
$this->touchIfTouching();
}
}

/**
* Attach a model to the parent using a custom class.
*
* @param mixed $id
*/
protected function attachUsingCustomClass($id, array $attributes)
{
$records = $this->formatAttachRecords(
$this->parseIds($id),
$attributes
);

foreach ($records as $record) {
$this->newPivot($record, false)->save();
}
}

/**
* Detach models from the relationship.
*
* @param mixed $ids
* @param bool $touch
*/
public function detach($ids = null, $touch = true)
{
if ($this->using) {
$results = $this->detachUsingCustomClass($ids);
} else {
return parent::detach($ids, $touch);
}

if ($touch) {
$this->touchIfTouching();
}

return $results;
}

/**
* Detach models from the relationship using a custom class.
*
* @param mixed $ids
* @return int
*/
protected function detachUsingCustomClass($ids)
{
$results = 0;

$pivots = $this->getCurrentlyAttachedPivots($ids);

foreach ($pivots as $pivot) {
$results += $pivot->delete();
}

return $results;
}

/**
* Update an existing pivot record on the table.
*
* @param mixed $id
* @param bool $touch
*/
public function updateExistingPivot($id, array $attributes, $touch = true)
{
if ($this->using) {
return $this->updateExistingPivotUsingCustomClass($id, $attributes, $touch);
}

return parent::updateExistingPivot($id, $attributes, $touch);
}

/**
* Update an existing pivot record on the table via a custom class.
*
* @param mixed $id
* @return int
*/
protected function updateExistingPivotUsingCustomClass($id, array $attributes, bool $touch)
{
$pivot = $this->getCurrentlyAttachedPivots($id)->first();

$updated = $pivot ? $pivot->fill($attributes)->isDirty() : false;

if ($updated) {
$pivot->save();
}

if ($touch) {
$this->touchIfTouching();
}

return (int) $updated;
}

/**
* Get the pivot models that are currently attached.
*
* @param mixed $ids
*/
protected function getCurrentlyAttachedPivots($ids = null): Collection
{
$query = $this->newPivotQuery();

if ($ids !== null) {
$query->whereIn($this->relatedPivotKey, $this->parseIds($ids));
}

return $query->get()->map(function ($record) {
/** @var class-string<Pivot> $class */
$class = $this->using ?: Pivot::class;

return $class::fromRawAttributes($this->parent, (array) $record, $this->getTable(), true)
->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey);
});
}

/**
* Create a new pivot model instance.
*
* Overrides parent to include pivotValues in the attributes.
*
* @param bool $exists
*/
public function newPivot(array $attributes = [], $exists = false)
{
$attributes = array_merge(
array_column($this->pivotValues, 'value', 'column'),
$attributes
);

/** @var Pivot $pivot */
$pivot = $this->related->newPivot(
$this->parent,
$attributes,
$this->table,
$exists,
$this->using
);

return $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey);
}
}
40 changes: 39 additions & 1 deletion src/core/src/Database/Eloquent/Relations/MorphPivot.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,46 @@

namespace Hypervel\Database\Eloquent\Relations;

use Hyperf\Database\Model\Relations\MorphPivot as BaseMorphPivot;
use Hyperf\DbConnection\Model\Relations\MorphPivot as BaseMorphPivot;
use Hypervel\Database\Eloquent\Concerns\HasAttributes;
use Hypervel\Database\Eloquent\Concerns\HasCallbacks;
use Hypervel\Database\Eloquent\Concerns\HasObservers;

class MorphPivot extends BaseMorphPivot
{
use HasAttributes;
use HasCallbacks;
use HasObservers;

/**
* Delete the pivot model record from the database.
*
* Overrides parent to fire deleting/deleted events even for composite key pivots,
* while maintaining the morph type constraint.
*/
public function delete(): mixed
{
// If pivot has a primary key, use parent's delete which fires events
if (isset($this->attributes[$this->getKeyName()])) {
return parent::delete();
}

// For composite key pivots, manually fire events around the raw delete
if ($this->fireModelEvent('deleting') === false) {
return 0;
}

$query = $this->getDeleteQuery();

// Add morph type constraint (from Hyperf's MorphPivot::delete())
$query->where($this->morphType, $this->morphClass);

$result = $query->delete();

$this->exists = false;

$this->fireModelEvent('deleted');

return $result;
}
}
43 changes: 43 additions & 0 deletions src/core/src/Database/Eloquent/Relations/MorphToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Hypervel\Database\Eloquent\Relations;

use Hyperf\Collection\Collection;
use Hyperf\Database\Model\Relations\MorphToMany as BaseMorphToMany;
use Hypervel\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable;
use Hypervel\Database\Eloquent\Relations\Concerns\WithoutAddConstraints;
use Hypervel\Database\Eloquent\Relations\Contracts\Relation as RelationContract;

Expand Down Expand Up @@ -42,14 +44,55 @@
*/
class MorphToMany extends BaseMorphToMany implements RelationContract
{
use InteractsWithPivotTable;
use WithoutAddConstraints;

/**
* Get the pivot models that are currently attached.
*
* Overrides trait to use MorphPivot and set morph type/class on the pivot models.
*
* @param mixed $ids
*/
protected function getCurrentlyAttachedPivots($ids = null): Collection
{
$query = $this->newPivotQuery();

if ($ids !== null) {
$query->whereIn($this->relatedPivotKey, $this->parseIds($ids));
}

return $query->get()->map(function ($record) {
/** @var class-string<MorphPivot> $class */
$class = $this->using ?: MorphPivot::class;

$pivot = $class::fromRawAttributes($this->parent, (array) $record, $this->getTable(), true)
->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey);

if ($pivot instanceof MorphPivot) {
$pivot->setMorphType($this->morphType)
->setMorphClass($this->morphClass);
}

return $pivot;
});
}

/**
* Create a new pivot model instance.
*
* Overrides parent to include pivotValues and set morph type/class.
*
* @param bool $exists
* @return TPivotModel
*/
public function newPivot(array $attributes = [], $exists = false)
{
$attributes = array_merge(
array_column($this->pivotValues, 'value', 'column'),
$attributes
);

$using = $this->using;

$pivot = $using ? $using::fromRawAttributes($this->parent, $attributes, $this->table, $exists)
Expand Down
34 changes: 33 additions & 1 deletion src/core/src/Database/Eloquent/Relations/Pivot.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,40 @@

namespace Hypervel\Database\Eloquent\Relations;

use Hyperf\Database\Model\Relations\Pivot as BasePivot;
use Hyperf\DbConnection\Model\Relations\Pivot as BasePivot;
use Hypervel\Database\Eloquent\Concerns\HasAttributes;
use Hypervel\Database\Eloquent\Concerns\HasCallbacks;
use Hypervel\Database\Eloquent\Concerns\HasObservers;

class Pivot extends BasePivot
{
use HasAttributes;
use HasCallbacks;
use HasObservers;

/**
* Delete the pivot model record from the database.
*
* Overrides parent to fire deleting/deleted events even for composite key pivots.
*/
public function delete(): mixed
{
// If pivot has a primary key, use parent's delete which fires events
if (isset($this->attributes[$this->getKeyName()])) {
return parent::delete();
}

// For composite key pivots, manually fire events around the raw delete
if ($this->fireModelEvent('deleting') === false) {
return 0;
}

$result = $this->getDeleteQuery()->delete();

$this->exists = false;

$this->fireModelEvent('deleted');

return $result;
}
}
Loading