209 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			PHP
		
	
	
			
		
		
	
	
			209 lines
		
	
	
		
			6.9 KiB
		
	
	
	
		
			PHP
		
	
	
<?php
 | 
						|
 | 
						|
/*
 | 
						|
 * This file is part of the Symfony package.
 | 
						|
 *
 | 
						|
 * (c) Fabien Potencier <fabien@symfony.com>
 | 
						|
 *
 | 
						|
 * For the full copyright and license information, please view the LICENSE
 | 
						|
 * file that was distributed with this source code.
 | 
						|
 */
 | 
						|
 | 
						|
namespace Symfony\Component\Uid;
 | 
						|
 | 
						|
/**
 | 
						|
 * A ULID is lexicographically sortable and contains a 48-bit timestamp and 80-bit of crypto-random entropy.
 | 
						|
 *
 | 
						|
 * @see https://github.com/ulid/spec
 | 
						|
 *
 | 
						|
 * @author Nicolas Grekas <p@tchwork.com>
 | 
						|
 */
 | 
						|
class Ulid extends AbstractUid implements TimeBasedUidInterface
 | 
						|
{
 | 
						|
    protected const NIL = '00000000000000000000000000';
 | 
						|
    protected const MAX = '7ZZZZZZZZZZZZZZZZZZZZZZZZZ';
 | 
						|
 | 
						|
    private static string $time = '';
 | 
						|
    private static array $rand = [];
 | 
						|
 | 
						|
    public function __construct(?string $ulid = null)
 | 
						|
    {
 | 
						|
        if (null === $ulid) {
 | 
						|
            $this->uid = static::generate();
 | 
						|
        } elseif (self::NIL === $ulid) {
 | 
						|
            $this->uid = $ulid;
 | 
						|
        } elseif (self::MAX === strtr($ulid, 'z', 'Z')) {
 | 
						|
            $this->uid = $ulid;
 | 
						|
        } else {
 | 
						|
            if (!self::isValid($ulid)) {
 | 
						|
                throw new \InvalidArgumentException(sprintf('Invalid ULID: "%s".', $ulid));
 | 
						|
            }
 | 
						|
 | 
						|
            $this->uid = strtoupper($ulid);
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    public static function isValid(string $ulid): bool
 | 
						|
    {
 | 
						|
        if (26 !== \strlen($ulid)) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        if (26 !== strspn($ulid, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) {
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        return $ulid[0] <= '7';
 | 
						|
    }
 | 
						|
 | 
						|
    public static function fromString(string $ulid): static
 | 
						|
    {
 | 
						|
        if (36 === \strlen($ulid) && preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $ulid)) {
 | 
						|
            $ulid = uuid_parse($ulid);
 | 
						|
        } elseif (22 === \strlen($ulid) && 22 === strspn($ulid, BinaryUtil::BASE58[''])) {
 | 
						|
            $ulid = str_pad(BinaryUtil::fromBase($ulid, BinaryUtil::BASE58), 16, "\0", \STR_PAD_LEFT);
 | 
						|
        }
 | 
						|
 | 
						|
        if (16 !== \strlen($ulid)) {
 | 
						|
            return match (strtr($ulid, 'z', 'Z')) {
 | 
						|
                self::NIL => new NilUlid(),
 | 
						|
                self::MAX => new MaxUlid(),
 | 
						|
                default => new static($ulid),
 | 
						|
            };
 | 
						|
        }
 | 
						|
 | 
						|
        $ulid = bin2hex($ulid);
 | 
						|
        $ulid = sprintf('%02s%04s%04s%04s%04s%04s%04s',
 | 
						|
            base_convert(substr($ulid, 0, 2), 16, 32),
 | 
						|
            base_convert(substr($ulid, 2, 5), 16, 32),
 | 
						|
            base_convert(substr($ulid, 7, 5), 16, 32),
 | 
						|
            base_convert(substr($ulid, 12, 5), 16, 32),
 | 
						|
            base_convert(substr($ulid, 17, 5), 16, 32),
 | 
						|
            base_convert(substr($ulid, 22, 5), 16, 32),
 | 
						|
            base_convert(substr($ulid, 27, 5), 16, 32)
 | 
						|
        );
 | 
						|
 | 
						|
        if (self::NIL === $ulid) {
 | 
						|
            return new NilUlid();
 | 
						|
        }
 | 
						|
 | 
						|
        if (self::MAX === $ulid = strtr($ulid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ')) {
 | 
						|
            return new MaxUlid();
 | 
						|
        }
 | 
						|
 | 
						|
        $u = new static(self::NIL);
 | 
						|
        $u->uid = $ulid;
 | 
						|
 | 
						|
        return $u;
 | 
						|
    }
 | 
						|
 | 
						|
    public function toBinary(): string
 | 
						|
    {
 | 
						|
        $ulid = strtr($this->uid, 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
 | 
						|
 | 
						|
        $ulid = sprintf('%02s%05s%05s%05s%05s%05s%05s',
 | 
						|
            base_convert(substr($ulid, 0, 2), 32, 16),
 | 
						|
            base_convert(substr($ulid, 2, 4), 32, 16),
 | 
						|
            base_convert(substr($ulid, 6, 4), 32, 16),
 | 
						|
            base_convert(substr($ulid, 10, 4), 32, 16),
 | 
						|
            base_convert(substr($ulid, 14, 4), 32, 16),
 | 
						|
            base_convert(substr($ulid, 18, 4), 32, 16),
 | 
						|
            base_convert(substr($ulid, 22, 4), 32, 16)
 | 
						|
        );
 | 
						|
 | 
						|
        return hex2bin($ulid);
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Returns the identifier as a base32 case insensitive string.
 | 
						|
     *
 | 
						|
     * @see https://tools.ietf.org/html/rfc4648#section-6
 | 
						|
     *
 | 
						|
     * @example 09EJ0S614A9FXVG9C5537Q9ZE1 (len=26)
 | 
						|
     */
 | 
						|
    public function toBase32(): string
 | 
						|
    {
 | 
						|
        return $this->uid;
 | 
						|
    }
 | 
						|
 | 
						|
    public function getDateTime(): \DateTimeImmutable
 | 
						|
    {
 | 
						|
        $time = strtr(substr($this->uid, 0, 10), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv');
 | 
						|
 | 
						|
        if (\PHP_INT_SIZE >= 8) {
 | 
						|
            $time = (string) hexdec(base_convert($time, 32, 16));
 | 
						|
        } else {
 | 
						|
            $time = sprintf('%02s%05s%05s',
 | 
						|
                base_convert(substr($time, 0, 2), 32, 16),
 | 
						|
                base_convert(substr($time, 2, 4), 32, 16),
 | 
						|
                base_convert(substr($time, 6, 4), 32, 16)
 | 
						|
            );
 | 
						|
            $time = BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10);
 | 
						|
        }
 | 
						|
 | 
						|
        if (4 > \strlen($time)) {
 | 
						|
            $time = '000'.$time;
 | 
						|
        }
 | 
						|
 | 
						|
        return \DateTimeImmutable::createFromFormat('U.u', substr_replace($time, '.', -3, 0));
 | 
						|
    }
 | 
						|
 | 
						|
    public static function generate(?\DateTimeInterface $time = null): string
 | 
						|
    {
 | 
						|
        if (null === $mtime = $time) {
 | 
						|
            $time = microtime(false);
 | 
						|
            $time = substr($time, 11).substr($time, 2, 3);
 | 
						|
        } elseif (0 > $time = $time->format('Uv')) {
 | 
						|
            throw new \InvalidArgumentException('The timestamp must be positive.');
 | 
						|
        }
 | 
						|
 | 
						|
        if ($time > self::$time || (null !== $mtime && $time !== self::$time)) {
 | 
						|
            randomize:
 | 
						|
            $r = unpack('n*', random_bytes(10));
 | 
						|
            $r[1] |= ($r[5] <<= 4) & 0xF0000;
 | 
						|
            $r[2] |= ($r[5] <<= 4) & 0xF0000;
 | 
						|
            $r[3] |= ($r[5] <<= 4) & 0xF0000;
 | 
						|
            $r[4] |= ($r[5] <<= 4) & 0xF0000;
 | 
						|
            unset($r[5]);
 | 
						|
            self::$rand = $r;
 | 
						|
            self::$time = $time;
 | 
						|
        } elseif ([1 => 0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) {
 | 
						|
            if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) {
 | 
						|
                $time = (string) (1 + $time);
 | 
						|
            } elseif ('999999999' === $mtime = substr($time, -9)) {
 | 
						|
                $time = (1 + substr($time, 0, -9)).'000000000';
 | 
						|
            } else {
 | 
						|
                $time = substr_replace($time, str_pad(++$mtime, 9, '0', \STR_PAD_LEFT), -9);
 | 
						|
            }
 | 
						|
 | 
						|
            goto randomize;
 | 
						|
        } else {
 | 
						|
            for ($i = 4; $i > 0 && 0xFFFFF === self::$rand[$i]; --$i) {
 | 
						|
                self::$rand[$i] = 0;
 | 
						|
            }
 | 
						|
 | 
						|
            ++self::$rand[$i];
 | 
						|
            $time = self::$time;
 | 
						|
        }
 | 
						|
 | 
						|
        if (\PHP_INT_SIZE >= 8) {
 | 
						|
            $time = base_convert($time, 10, 32);
 | 
						|
        } else {
 | 
						|
            $time = str_pad(bin2hex(BinaryUtil::fromBase($time, BinaryUtil::BASE10)), 12, '0', \STR_PAD_LEFT);
 | 
						|
            $time = sprintf('%s%04s%04s',
 | 
						|
                base_convert(substr($time, 0, 2), 16, 32),
 | 
						|
                base_convert(substr($time, 2, 5), 16, 32),
 | 
						|
                base_convert(substr($time, 7, 5), 16, 32)
 | 
						|
            );
 | 
						|
        }
 | 
						|
 | 
						|
        return strtr(sprintf('%010s%04s%04s%04s%04s',
 | 
						|
            $time,
 | 
						|
            base_convert(self::$rand[1], 10, 32),
 | 
						|
            base_convert(self::$rand[2], 10, 32),
 | 
						|
            base_convert(self::$rand[3], 10, 32),
 | 
						|
            base_convert(self::$rand[4], 10, 32)
 | 
						|
        ), 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ');
 | 
						|
    }
 | 
						|
}
 |