Added full support for emojis

This commit is contained in:
Alejandro Celaya 2022-01-10 13:04:16 +01:00
parent b941ee9aa9
commit ce47d8c591
6 changed files with 107 additions and 11 deletions

View File

@ -21,6 +21,13 @@ return (static function (): array {
'mssql' => '1433', 'mssql' => '1433',
default => '3306', default => '3306',
}; };
$resolveCharset = static fn () => match ($driver) {
// This does not determine charsets or collations in tables or columns, but the charset used in the data
// flowing in the connection, so it has to match what has been set in the database.
'maria', 'mysql' => 'utf8mb4',
'postgres' => 'utf8',
default => null,
};
$resolveConnection = static fn () => match ($driver) { $resolveConnection = static fn () => match ($driver) {
null, 'sqlite' => [ null, 'sqlite' => [
'driver' => 'pdo_sqlite', 'driver' => 'pdo_sqlite',
@ -34,7 +41,7 @@ return (static function (): array {
'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null), 'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null),
'port' => env('DB_PORT', $resolveDefaultPort()), 'port' => env('DB_PORT', $resolveDefaultPort()),
'unix_socket' => $isMysqlCompatible ? env('DB_UNIX_SOCKET') : null, 'unix_socket' => $isMysqlCompatible ? env('DB_UNIX_SOCKET') : null,
'charset' => 'utf8', 'charset' => $resolveCharset(),
], ],
}; };

View File

@ -55,6 +55,7 @@ $buildDbConnection = static function (): array {
'user' => 'postgres', 'user' => 'postgres',
'password' => 'root', 'password' => 'root',
'dbname' => 'shlink_test', 'dbname' => 'shlink_test',
'charset' => 'utf8',
], ],
'mssql' => [ 'mssql' => [
'driver' => 'pdo_sqlsrv', 'driver' => 'pdo_sqlsrv',
@ -70,6 +71,7 @@ $buildDbConnection = static function (): array {
'user' => 'root', 'user' => 'root',
'password' => 'root', 'password' => 'root',
'dbname' => 'shlink_test', 'dbname' => 'shlink_test',
'charset' => 'utf8mb4',
], ],
}; };
}; };

View File

@ -11,7 +11,7 @@ use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
use function get_class; use function is_subclass_of;
/** /**
* Auto-generated Migration: Please modify to your needs! * Auto-generated Migration: Please modify to your needs!
@ -24,13 +24,15 @@ class Version20160819142757 extends AbstractMigration
*/ */
public function up(Schema $schema): void public function up(Schema $schema): void
{ {
$platformClass = get_class($this->connection->getDatabasePlatform()); $platformClass = $this->connection->getDatabasePlatform();
$table = $schema->getTable('short_urls'); $table = $schema->getTable('short_urls');
$column = $table->getColumn('short_code'); $column = $table->getColumn('short_code');
match ($platformClass) { match (true) {
MySQLPlatform::class => $column->setPlatformOption('collation', 'utf8_bin'), is_subclass_of($platformClass, MySQLPlatform::class) => $column
SqlitePlatform::class => $column->setPlatformOption('collate', 'BINARY'), ->setPlatformOption('charset', 'utf8mb4')
->setPlatformOption('collation', 'utf8mb4_bin'),
is_subclass_of($platformClass, SqlitePlatform::class) => $column->setPlatformOption('collate', 'BINARY'),
default => null, default => null,
}; };
} }

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20220110113313 extends AbstractMigration
{
private const CHARSET = 'utf8mb4';
private const COLLATIONS = [
'short_urls' => [
'original_url' => 'unicode_ci',
'short_code' => 'bin',
'import_original_short_code' => 'unicode_ci',
'title' => 'unicode_ci',
],
'domains' => [
'authority' => 'unicode_ci',
'base_url_redirect' => 'unicode_ci',
'regular_not_found_redirect' => 'unicode_ci',
'invalid_short_url_redirect' => 'unicode_ci',
],
'tags' => [
'name' => 'unicode_ci',
],
'visits' => [
'referer' => 'unicode_ci',
'user_agent' => 'unicode_ci',
'visited_url' => 'unicode_ci',
],
'visit_locations' => [
'country_code' => 'unicode_ci',
'country_name' => 'unicode_ci',
'region_name' => 'unicode_ci',
'city_name' => 'unicode_ci',
'timezone' => 'unicode_ci',
],
];
public function up(Schema $schema): void
{
$this->skipIf(! $this->isMySql(), 'This only sets MySQL-specific database options');
foreach (self::COLLATIONS as $tableName => $columns) {
$table = $schema->getTable($tableName);
foreach ($columns as $columnName => $collation) {
$table->getColumn($columnName)
->setPlatformOption('charset', self::CHARSET)
->setPlatformOption('collation', self::CHARSET . '_' . $collation);
}
}
}
public function down(Schema $schema): void
{
// No down
}
public function isTransactional(): bool
{
return ! $this->isMySql();
}
private function isMySql(): bool
{
return $this->connection->getDatabasePlatform() instanceof MySQLPlatform;
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace <namespace>; namespace <namespace>;
use Doctrine\DBAL\Platforms\MySQLPlatform;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
@ -21,6 +22,6 @@ final class <className> extends AbstractMigration
public function isTransactional(): bool public function isTransactional(): bool
{ {
return $this->connection->getDatabasePlatform()->getName() !== 'mysql'; return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
} }
} }

View File

@ -315,11 +315,22 @@ class CreateShortUrlTest extends ApiTestCase
yield ['https://mobile.twitter.com/shlinkio/status/1360637738421268481']; yield ['https://mobile.twitter.com/shlinkio/status/1360637738421268481'];
} }
/** @test */
public function canCreateShortUrlsWithEmojis(): void
{
[$statusCode, $payload] = $this->createShortUrl([
'longUrl' => 'https://emojipedia.org/fire/',
'title' => '🔥🔥🔥',
'customSlug' => '🦣🦣🦣',
]);
self::assertEquals(self::STATUS_OK, $statusCode);
self::assertEquals('🔥🔥🔥', $payload['title']);
self::assertEquals('🦣🦣🦣', $payload['shortCode']);
self::assertEquals('http://doma.in/🦣🦣🦣', $payload['shortUrl']);
}
/** /**
* @return array { * @return array{int $statusCode, array $payload}
* @var int $statusCode
* @var array $payload
* }
*/ */
private function createShortUrl(array $body = [], string $apiKey = 'valid_api_key'): array private function createShortUrl(array $body = [], string $apiKey = 'valid_api_key'): array
{ {