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

Add Cuid2 Support #54375

Draft
wants to merge 20 commits into
base: 11.x
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"php": "^8.2",
"ext-ctype": "*",
"ext-filter": "*",
"ext-gmp": "*",
"ext-hash": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
Expand Down Expand Up @@ -57,6 +58,7 @@
"symfony/uid": "^7.0.3",
"symfony/var-dumper": "^7.0.3",
"tijsverkoyen/css-to-inline-styles": "^2.2.5",
"visus/cuid2": "^5.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This maintainer isn't very active. I'd be very hesitant to base the framework on this package's support.

"vlucas/phpdotenv": "^5.6.1",
"voku/portable-ascii": "^2.0.2"
},
Expand Down Expand Up @@ -98,7 +100,6 @@
"spatie/once": "*"
},
"require-dev": {
"ext-gmp": "*",
"ably/ably-php": "^1.0",
"aws/aws-sdk-php": "^3.322.9",
"fakerphp/faker": "^1.24",
Expand Down
13 changes: 13 additions & 0 deletions config/cuid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| Cuid Options
|--------------------------------------------------------------------------
|
| This will allow you to control the length of your cuid id generation, 24 by default
|
*/
'length' => env('CUID_LENGTH', 24),
];
32 changes: 32 additions & 0 deletions src/Illuminate/Database/Eloquent/Concerns/HasCuid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Illuminate\Database\Eloquent\Concerns;

use Illuminate\Support\Str;
use Visus\Cuid2\Cuid2;

trait HasCuid
{
use HasUniqueStringIds;

/**
* Generate a new unique key for the model.
*/
public function newUniqueId(): string
{
$size = intval(config('cuid.length', 24));
$cuid = new Cuid2(maxLength: ($size < 2 || $size > 32) ? 24 : $size);

return $cuid->toString();
}

/**
* Determine if given key is valid.
*
* @param mixed $value
*/
protected function isValidUniqueId($value): bool
{
return Str::isCuid($value);
}
}
71 changes: 71 additions & 0 deletions src/Illuminate/Database/Schema/Blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Closure;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Concerns\HasCuid;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Query\Expression;
use Illuminate\Database\Schema\Grammars\Grammar;
Expand Down Expand Up @@ -1053,6 +1054,12 @@ public function foreignIdFor($model, $column = null)
->referencesModelColumn($model->getKeyName());
}

if (in_array(HasCuid::class, $modelTraits, true)) {
return $this->foreignCuid($column, 32)
->table($model->getTable())
->referencesModelColumn($model->getKeyName());
}

return $this->foreignUuid($column)
->table($model->getTable())
->referencesModelColumn($model->getKeyName());
Expand Down Expand Up @@ -1399,6 +1406,34 @@ public function foreignUlid($column, $length = 26)
]));
}

/**
* Create a new CUID column on the table.
*
* @param string $column
* @param int|null $length
* @return \Illuminate\Database\Schema\ColumnDefinition
*/
public function cuid($column = 'cuid', $length = 32)
{
return $this->char($column, $length);
}

/**
* Create a new CUID column on the table with a foreign key constraint.
*
* @param string $column
* @param int|null $length
* @return \Illuminate\Database\Schema\ForeignIdColumnDefinition
*/
public function foreignCuid($column, $length = 32)
{
return $this->addColumnDefinition(new ForeignIdColumnDefinition($this, [
'type' => 'char',
'name' => $column,
'length' => $length,
]));
}

/**
* Create a new IP address column on the table.
*
Expand Down Expand Up @@ -1486,6 +1521,8 @@ public function morphs($name, $indexName = null)
$this->uuidMorphs($name, $indexName);
} elseif (Builder::$defaultMorphKeyType === 'ulid') {
$this->ulidMorphs($name, $indexName);
} elseif (Builder::$defaultMorphKeyType === 'cuid') {
$this->cuidMorphs($name, $indexName);
} else {
$this->numericMorphs($name, $indexName);
}
Expand All @@ -1504,6 +1541,8 @@ public function nullableMorphs($name, $indexName = null)
$this->nullableUuidMorphs($name, $indexName);
} elseif (Builder::$defaultMorphKeyType === 'ulid') {
$this->nullableUlidMorphs($name, $indexName);
} elseif (Builder::$defaultMorphKeyType === 'cuid') {
$this->nullableCuidMorphs($name, $indexName);
} else {
$this->nullableNumericMorphs($name, $indexName);
}
Expand Down Expand Up @@ -1605,6 +1644,38 @@ public function nullableUlidMorphs($name, $indexName = null)
$this->index(["{$name}_type", "{$name}_id"], $indexName);
}

/**
* Add the proper columns for a polymorphic table using CUIDs.
*
* @param string $name
* @param string|null $indexName
* @return void
*/
public function cuidMorphs($name, $indexName = null)
{
$this->string("{$name}_type");

$this->cuid("{$name}_id", 32);

$this->index(["{$name}_type", "{$name}_id"], $indexName);
}

/**
* Add nullable columns for a polymorphic table using CUIDs.
*
* @param string $name
* @param string|null $indexName
* @return void
*/
public function nullableCuidMorphs($name, $indexName = null)
{
$this->string("{$name}_type")->nullable();

$this->cuid("{$name}_id", 32)->nullable();

$this->index(["{$name}_type", "{$name}_id"], $indexName);
}

/**
* Add the `remember_token` column to the table.
*
Expand Down
14 changes: 12 additions & 2 deletions src/Illuminate/Database/Schema/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ public static function defaultStringLength($length)
*/
public static function defaultMorphKeyType(string $type)
{
if (! in_array($type, ['int', 'uuid', 'ulid'])) {
throw new InvalidArgumentException("Morph key type must be 'int', 'uuid', or 'ulid'.");
if (! in_array($type, ['int', 'uuid', 'ulid', 'cuid'])) {
throw new InvalidArgumentException("Morph key type must be 'int', 'uuid', 'ulid', or 'cuid'.");
}

static::$defaultMorphKeyType = $type;
Expand All @@ -108,6 +108,16 @@ public static function morphUsingUlids()
static::defaultMorphKeyType('ulid');
}

/**
* Set the default morph key type for migrations to CUIDs.
*
* @return void
*/
public static function morphUsingCuids()
{
static::defaultMorphKeyType('cuid');
}

/**
* Create a database in the schema.
*
Expand Down
25 changes: 25 additions & 0 deletions src/Illuminate/Support/Str.php
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,31 @@ public static function isUlid($value)
return Ulid::isValid($value);
}

/**
* Determine if a given value is a valid CUID.
*
* @param mixed $value
* @return bool
*/
public static function isCuid($value)
{
if (! is_string($value)) {
return false;
}

// validate length
$minLength = 2;
$maxLength = 32;

$length = strlen($value);

if ($length >= $minLength && $length <= $maxLength && preg_match('/^[a-z][0-9a-z]+$/', $value)) {
return true;
}

return false;
}

/**
* Convert a string to kebab case.
*
Expand Down
75 changes: 75 additions & 0 deletions tests/Database/DatabaseEloquentModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use Illuminate\Database\Eloquent\Casts\AsStringable;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Concerns\HasCuid;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Factories\Factory;
Expand Down Expand Up @@ -2058,6 +2059,52 @@ public function testCloneModelMakesAFreshCopyOfTheModelWhenModelHasUlid()
$this->assertEquals(['bar'], $clone->foo);
}

public function testCloneModelMakesAFreshCopyOfTheModelWhenModelHasCuidPrimaryKey()
{
$class = new EloquentPrimaryCuidModelStub();
$class->cuid = 'uzmbdw1jp9yvi2nm3s8zx27p';
$class->exists = true;
$class->first = 'taylor';
$class->last = 'otwell';
$class->created_at = $class->freshTimestamp();
$class->updated_at = $class->freshTimestamp();
$class->setRelation('foo', ['bar']);

$clone = $class->replicate();

$this->assertNull($clone->cuid);
$this->assertFalse($clone->exists);
$this->assertSame('taylor', $clone->first);
$this->assertSame('otwell', $clone->last);
$this->assertArrayNotHasKey('created_at', $clone->getAttributes());
$this->assertArrayNotHasKey('updated_at', $clone->getAttributes());
$this->assertEquals(['bar'], $clone->foo);
}

public function testCloneModelMakesAFreshCopyOfTheModelWhenModelHasCuid()
{
$class = new EloquentNonPrimaryCuidModelStub();
$class->id = 1;
$class->cuid = 'uzmbdw1jp9yvi2nm3s8zx27p';
$class->exists = true;
$class->first = 'taylor';
$class->last = 'otwell';
$class->created_at = $class->freshTimestamp();
$class->updated_at = $class->freshTimestamp();
$class->setRelation('foo', ['bar']);

$clone = $class->replicate();

$this->assertNull($clone->id);
$this->assertNull($clone->cuid);
$this->assertFalse($clone->exists);
$this->assertSame('taylor', $clone->first);
$this->assertSame('otwell', $clone->last);
$this->assertArrayNotHasKey('created_at', $clone->getAttributes());
$this->assertArrayNotHasKey('updated_at', $clone->getAttributes());
$this->assertEquals(['bar'], $clone->foo);
}

public function testModelObserversCanBeAttachedToModels()
{
EloquentModelStub::setEventDispatcher($events = m::mock(Dispatcher::class));
Expand Down Expand Up @@ -3728,6 +3775,34 @@ public function uniqueIds()
}
}

class EloquentPrimaryCuidModelStub extends EloquentModelStub
{
use HasCuid;

public $incrementing = false;
protected $keyType = 'string';

public function getKeyName()
{
return 'cuid';
}
}

class EloquentNonPrimaryCuidModelStub extends EloquentModelStub
{
use HasCuid;

public function getKeyName()
{
return 'id';
}

public function uniqueIds()
{
return ['cuid'];
}
}

#[ObservedBy(EloquentTestObserverStub::class)]
class EloquentModelWithObserveAttributeStub extends EloquentModelStub
{
Expand Down
Loading