Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"role" => GeneaLabs\LaravelGovernor\Role::class,
"team" => GeneaLabs\LaravelGovernor\Team::class,
"invitation" => GeneaLabs\LaravelGovernor\TeamInvitation::class,
"ownable" => GeneaLabs\LaravelGovernor\GovernorOwnable::class,
],

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

declare(strict_types=1);

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateGovernorOwnablesTable extends Migration
{
public function __construct()
{
if (app()->bound("Hyn\Tenancy\Environment")) {
$this->connection = config("tenancy.db.tenant-connection-name");
}
}

public function up(): void
{
Schema::create('governor_ownables', function (Blueprint $table): void {
$table->id();
$table->morphs('ownable');
$table->unsignedBigInteger('user_id');
$table->timestamps();

$table->unique(['ownable_type', 'ownable_id']);
});
}

public function down(): void
{
Schema::dropIfExists('governor_ownables');
}
}
90 changes: 90 additions & 0 deletions database/seeders/LaravelGovernorUpgradeTo0130.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace GeneaLabs\LaravelGovernor\Database\Seeders;

use GeneaLabs\LaravelGovernor\Traits\EntityManagement;
use Illuminate\Container\Container;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Seeder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;

class LaravelGovernorUpgradeTo0130 extends Seeder
{
use EntityManagement;

public function run(): void
{
$this->migrateOwnedByData();
}

protected function migrateOwnedByData(): void
{
$this->getModels()
->each(function (string $modelClass): void {
$model = new $modelClass;
$table = $model->getTable();
$connection = $model->getConnectionName();

if (! Schema::connection($connection)->hasColumn($table, 'governor_owned_by')) {
return;
}

$records = DB::connection($connection)
->table($table)
->whereNotNull('governor_owned_by')
->select([$model->getKeyName(), 'governor_owned_by'])
->get();

foreach ($records as $record) {
$keyName = $model->getKeyName();

DB::table('governor_ownables')->insertOrIgnore([
'ownable_type' => $modelClass,
'ownable_id' => $record->{$keyName},
'user_id' => $record->governor_owned_by,
'created_at' => now(),
'updated_at' => now(),
]);
}
});
}

protected function getModels(): Collection
{
if (! is_dir(app_path())) {
return collect();
}

return collect(File::allFiles(app_path()))
->map(function ($item) {
$path = $item->getRelativePathName();
$class = sprintf(
'\%s%s',
Container::getInstance()->getNamespace(),
strtr(substr($path, 0, strrpos($path, '.')), '/', '\\'),
);

return $class;
})
->filter(function ($class) {
if (! class_exists($class)) {
return false;
}

$reflection = new \ReflectionClass($class);

return $reflection->isSubclassOf(Model::class)
&& ! $reflection->isAbstract()
&& in_array(
'GeneaLabs\LaravelGovernor\Traits\Governable',
class_uses_recursive($class),
);
})
->values();
}
}
33 changes: 33 additions & 0 deletions src/GovernorOwnable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace GeneaLabs\LaravelGovernor;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;

class GovernorOwnable extends Model
{
protected $table = 'governor_ownables';

protected $fillable = [
'ownable_type',
'ownable_id',
'user_id',
];

public function ownable(): MorphTo
{
return $this->morphTo();
}

public function owner(): BelongsTo
{
return $this->belongsTo(
config("genealabs-laravel-governor.models.auth"),
"user_id"
);
}
}
85 changes: 72 additions & 13 deletions src/Listeners/CreatedListener.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<?php namespace GeneaLabs\LaravelGovernor\Listeners;
<?php

declare(strict_types=1);

namespace GeneaLabs\LaravelGovernor\Listeners;

use GeneaLabs\LaravelGovernor\GovernorOwnable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;

Expand All @@ -18,20 +23,74 @@ public function handle(string $event, array $models)

collect($models)
->filter(function ($model) {
return $model instanceof Model
&& get_class($model) === config('genealabs-laravel-governor.models.auth');
return $model instanceof Model;
})
->each(function ($model) {
try {
$model->roles()->syncWithoutDetaching('Member');
} catch (Exception $exception) {
$roleClass = config("genealabs-laravel-governor.models.role");
(new $roleClass)->firstOrCreate([
'name' => 'Member',
'description' => 'Represents the baseline registered user. Customize permissions as best suits your site.',
]);
$model->roles()->attach('Member');
}
$this->assignDefaultRole($model);
$this->createOwnershipRecord($model);
});
}

protected function assignDefaultRole(Model $model): void
{
if (get_class($model) !== config('genealabs-laravel-governor.models.auth')) {
return;
}

try {
$model->roles()->syncWithoutDetaching('Member');
} catch (\Exception $exception) {
$roleClass = config("genealabs-laravel-governor.models.role");
(new $roleClass)->firstOrCreate([
'name' => 'Member',
'description' => 'Represents the baseline registered user. Customize permissions as best suits your site.',
]);
$model->roles()->attach('Member');
}
}

protected function createOwnershipRecord(Model $model): void
{
if (! in_array(
'GeneaLabs\LaravelGovernor\Traits\Governable',
class_uses_recursive($model),
)) {
return;
}

$ownerId = $model->_governor_pending_owner_id ?? null;
unset($model->_governor_pending_owner_id);

// Use the column value if explicitly set (deprecated but maintained for backward compat)
if (! $ownerId) {
// Check raw attributes directly, don't use accessor
$attrs = $model->getAttributes();
if (isset($attrs['governor_owned_by'])) {
$ownerId = $attrs['governor_owned_by'];
}
}

if (! $ownerId && auth()->check()) {
$ownerId = auth()->user()->id;
}

if (! $ownerId) {
return;
}

$ownableClass = config(
"genealabs-laravel-governor.models.ownable",
GovernorOwnable::class,
);

(new $ownableClass)->firstOrCreate(
[
'ownable_type' => get_class($model),
'ownable_id' => $model->getKey(),
],
[
'user_id' => $ownerId,
],
);
}
}
13 changes: 11 additions & 2 deletions src/Listeners/CreatingInvitationListener.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<?php namespace GeneaLabs\LaravelGovernor\Listeners;
<?php

declare(strict_types=1);

namespace GeneaLabs\LaravelGovernor\Listeners;

use Ramsey\Uuid\Uuid;

Expand All @@ -10,6 +14,11 @@ class CreatingInvitationListener
public function handle($model)
{
$model->token = Uuid::uuid4();
$model->ownedBy()->associate(auth()->user());

// Set deprecated governor_owned_by column for backward compatibility.
// Polymorphic ownership record is created by CreatedListener after save.
if (auth()->check()) {
$model->governor_owned_by = auth()->user()->getKey();
}
}
}
14 changes: 10 additions & 4 deletions src/Listeners/CreatingListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,16 @@ class_uses_recursive($model)
->each(function ($model) {
$model->getEntityFromModel(get_class($model));

if (
! $model->governor_owned_by
&& auth()->check()
) {
// Deprecated: governor_owned_by column is maintained for backward
// compatibility but ownership is now tracked in governor_ownables.
// The column will be removed in a future release.
//
// Check if governor_owned_by was explicitly set (before save).
// Access the raw attributes directly to bypass the accessor.
$attrs = $model->getAttributes();
$explicit = $attrs['governor_owned_by'] ?? null;

if (! $explicit && auth()->check()) {
$model->governor_owned_by = auth()->user()->id;
}
});
Expand Down
6 changes: 4 additions & 2 deletions src/Notifications/TeamInvitation.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ public function via($notifiable) : array
public function toMail($notifiable) : MailMessage
{
$appUrl = config("app.url");
$ownerName = $this->invitation->ownedBy?->name ?? "A team admin";
$teamName = $this->invitation->team?->name ?? "the team";
$message = [
"You have been invited by {$this->invitation->ownedBy->name}",
"to join team '{$this->invitation->team->name}' on {$appUrl}."
"You have been invited by {$ownerName}",
"to join team '{$teamName}' on {$appUrl}."
];
$route = route(
"genealabs.laravel-governor.invitations.update",
Expand Down
12 changes: 7 additions & 5 deletions src/Policies/BasePolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,13 @@ protected function validatePermissions(

$ownership = 'other';

if (
$model
&& $user->getKey() == $model->governor_owned_by
) {
$ownership = 'own';
if ($model) {
$ownable = $model->governorOwner;
$ownerId = $ownable?->user_id;

if ($ownerId !== null && $user->getKey() == $ownerId) {
$ownership = 'own';
}
}

$filteredPermissions = $this->filterPermissions($action, $entity, $ownership);
Expand Down
26 changes: 25 additions & 1 deletion src/Team.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ public function invitations(): HasMany
);
}

/**
* @deprecated Use governorOwner() relationship instead.
*/
public function owner(): BelongsTo
{
$authClass = config("genealabs-laravel-governor.models.auth");
Expand Down Expand Up @@ -100,17 +103,38 @@ public function removeMember(Model $user): void

public function getOwnerNameAttribute(): string
{
return $this->owner->name
return $this->governorOwner?->owner?->name
?? $this->owner?->name
?? "";
}

public function transferOwnership(Model $newOwner): self
{
$this->loadMissing('members');

// Update polymorphic ownership
$ownableClass = config(
"genealabs-laravel-governor.models.ownable",
\GeneaLabs\LaravelGovernor\GovernorOwnable::class,
);

(new $ownableClass)->updateOrCreate(
[
'ownable_type' => get_class($this),
'ownable_id' => $this->getKey(),
],
[
'user_id' => $newOwner->getKey(),
],
);

// Deprecated: maintain governor_owned_by for backward compatibility
$this->governor_owned_by = $newOwner->getKey();
$this->save();

// Clear cached relationship
$this->unsetRelation('governorOwner');

return $this;
}
}
Loading
Loading