|
|
|
|
@@ -2,40 +2,55 @@
|
|
|
|
|
|
|
|
|
|
namespace Gazelle;
|
|
|
|
|
|
|
|
|
|
use Requests;
|
|
|
|
|
use Torrents;
|
|
|
|
|
use OutOfBoundsException;
|
|
|
|
|
use UnexpectedValueException;
|
|
|
|
|
|
|
|
|
|
class Artist extends Base {
|
|
|
|
|
protected $id;
|
|
|
|
|
protected $revision;
|
|
|
|
|
protected $artistRole; // what different roles does an artist have
|
|
|
|
|
protected $nrGroups; // number of distinct groups
|
|
|
|
|
protected $groupRole; // what role does this artist have in a group
|
|
|
|
|
protected $groupList; // the release groups ordered by year and name
|
|
|
|
|
protected $sections; // their groups, gathered into sections
|
|
|
|
|
protected $id = 0;
|
|
|
|
|
protected $revisionId = 0;
|
|
|
|
|
protected $artistRole;
|
|
|
|
|
protected $nrGroups;
|
|
|
|
|
protected $groupRole;
|
|
|
|
|
/** Release groups ordered by year and name */
|
|
|
|
|
protected $groupList;
|
|
|
|
|
/** Their groups, gathered into sections */
|
|
|
|
|
protected $sections;
|
|
|
|
|
|
|
|
|
|
protected $discogsId;
|
|
|
|
|
protected $discogsName;
|
|
|
|
|
protected $discogsStem;
|
|
|
|
|
/** @var string|int */
|
|
|
|
|
protected $discogsSequence;
|
|
|
|
|
protected $discogsIsPreferred;
|
|
|
|
|
protected $homonyms;
|
|
|
|
|
|
|
|
|
|
protected $name;
|
|
|
|
|
protected $name = '';
|
|
|
|
|
protected $image;
|
|
|
|
|
protected $body;
|
|
|
|
|
/** @var bool|int */
|
|
|
|
|
protected $vanity;
|
|
|
|
|
protected $similar;
|
|
|
|
|
protected $similar = [];
|
|
|
|
|
|
|
|
|
|
protected $nrLeechers;
|
|
|
|
|
protected $nrSnatches;
|
|
|
|
|
protected $nrSeeders;
|
|
|
|
|
protected $nrTorrents;
|
|
|
|
|
|
|
|
|
|
const CACHE_PREFIX = 'artist_';
|
|
|
|
|
const DISCOGS_API_URL = 'https://api.discogs.com/artists/%d';
|
|
|
|
|
protected const CACHE_PREFIX = 'artist_';
|
|
|
|
|
protected const DISCOGS_API_URL = 'https://api.discogs.com/artists/%d';
|
|
|
|
|
|
|
|
|
|
public function __construct (int $id, $revision = false) {
|
|
|
|
|
/**
|
|
|
|
|
* Artist constructor.
|
|
|
|
|
* @param int $id
|
|
|
|
|
* @param int|null $revisionId
|
|
|
|
|
* @throws OutOfBoundsException
|
|
|
|
|
*/
|
|
|
|
|
public function __construct (int $id, $revisionId = null) {
|
|
|
|
|
parent::__construct();
|
|
|
|
|
$this->id = $id;
|
|
|
|
|
$this->revision = $revision;
|
|
|
|
|
$this->revisionId = $revisionId ?? 0;
|
|
|
|
|
|
|
|
|
|
$cacheKey = $this->cacheKey();
|
|
|
|
|
if (($info = $this->cache->get_value($cacheKey)) !== false) {
|
|
|
|
|
@@ -61,7 +76,7 @@ class Artist extends Base {
|
|
|
|
|
|
|
|
|
|
$this->db->prepared_query($sql, $args);
|
|
|
|
|
if (!$this->db->has_results()) {
|
|
|
|
|
throw new \Exception("Artist:not-found");
|
|
|
|
|
throw new OutOfBoundsException("Artist:not-found");
|
|
|
|
|
}
|
|
|
|
|
[$this->name, $this->image, $this->body, $this->vanity, $this->discogsId, $this->discogsName,
|
|
|
|
|
$this->discogsStem, $this->discogsSequence, $this->discogsIsPreferred
|
|
|
|
|
@@ -88,13 +103,12 @@ class Artist extends Base {
|
|
|
|
|
$this->cache->cache_value($cacheKey, [
|
|
|
|
|
$this->name, $this->image, $this->body, $this->vanity, $this->similar,
|
|
|
|
|
$this->discogsId, $this->discogsName, $this->discogsStem, $this->discogsSequence, $this->discogsIsPreferred, $this->homonyms
|
|
|
|
|
],
|
|
|
|
|
3600
|
|
|
|
|
], 3600
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function rename(int $userId, int $aliasId, string $name) {
|
|
|
|
|
public function rename(int $userId, int $aliasId, string $name): void {
|
|
|
|
|
$this->db->prepared_query("
|
|
|
|
|
INSERT INTO artists_alias
|
|
|
|
|
(ArtistID, Name, UserID, Redirect)
|
|
|
|
|
@@ -107,7 +121,7 @@ class Artist extends Base {
|
|
|
|
|
", $targetId, $aliasId
|
|
|
|
|
);
|
|
|
|
|
$this->db->prepared_query("
|
|
|
|
|
UPDATE artists_group SET Name = ? WHERE ArtistID = ?
|
|
|
|
|
UPDATE artists_group SET Name = ? WHERE ArtistID = ?
|
|
|
|
|
", $name, $this->id
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
@@ -116,14 +130,14 @@ class Artist extends Base {
|
|
|
|
|
SELECT GroupID FROM torrents_artists WHERE AliasID = ?
|
|
|
|
|
", $aliasId
|
|
|
|
|
);
|
|
|
|
|
$Groups = $this->db->collect('GroupID');
|
|
|
|
|
$groups = $this->db->collect('GroupID');
|
|
|
|
|
$this->db->prepared_query("
|
|
|
|
|
UPDATE IGNORE torrents_artists SET AliasID = ? WHERE AliasID = ?
|
|
|
|
|
", $targetId, $aliasId
|
|
|
|
|
);
|
|
|
|
|
foreach ($Groups as $GroupID) {
|
|
|
|
|
$this->cache->delete_value("groups_artists_$GroupID"); // Delete group artist cache
|
|
|
|
|
\Torrents::update_hash($GroupID);
|
|
|
|
|
foreach ($groups as $groupId) {
|
|
|
|
|
$this->cache->delete_value("groups_artists_$groupId"); // Delete group artist cache
|
|
|
|
|
Torrents::update_hash($groupId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// process artists in requests
|
|
|
|
|
@@ -133,46 +147,34 @@ class Artist extends Base {
|
|
|
|
|
WHERE AliasID = ?
|
|
|
|
|
", $aliasId
|
|
|
|
|
);
|
|
|
|
|
$Requests = $this->db->collect('RequestID');
|
|
|
|
|
$requests = $this->db->collect('RequestID');
|
|
|
|
|
$this->db->prepared_query("
|
|
|
|
|
UPDATE IGNORE requests_artists SET AliasID = ? WHERE AliasID = ?
|
|
|
|
|
", $targetId, $aliasId
|
|
|
|
|
);
|
|
|
|
|
foreach ($Requests as $RequestID) {
|
|
|
|
|
$this->cache->delete_value("request_artists_$RequestID"); // Delete request artist cache
|
|
|
|
|
\Requests::update_sphinx_requests($RequestID);
|
|
|
|
|
foreach ($requests as $requestId) {
|
|
|
|
|
$this->cache->delete_value("request_artists_$requestId"); // Delete request artist cache
|
|
|
|
|
Requests::update_sphinx_requests($requestId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function cacheKey() {
|
|
|
|
|
public function cacheKey(): string {
|
|
|
|
|
// TODO: change to protected when sections/ajax/artist.php is refactored
|
|
|
|
|
if ($this->revision) {
|
|
|
|
|
return self::CACHE_PREFIX . $this->id . '_r' . $this->revision;
|
|
|
|
|
} else {
|
|
|
|
|
return self::CACHE_PREFIX . $this->id;
|
|
|
|
|
}
|
|
|
|
|
return self::CACHE_PREFIX . $this->id
|
|
|
|
|
. ($this->revisionId ? '_r' . $this->revisionId : '');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function flushCache() {
|
|
|
|
|
public function flushCache(): void {
|
|
|
|
|
$this->cache->delete_value($this->cacheKey());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function resolveAlias(string $name) {
|
|
|
|
|
$this->db->_prepared_query("
|
|
|
|
|
SELECT AliasID, ArtistID, Name, Redirect
|
|
|
|
|
FROM artists_alias
|
|
|
|
|
WHERE Name = ?
|
|
|
|
|
", $name
|
|
|
|
|
);
|
|
|
|
|
while ([$CloneAliasID, $CloneArtistID, $CloneAliasName, $CloneRedirect] = $this->db->next_record(MYSQLI_NUM, false)) {
|
|
|
|
|
if (!strcasecmp($CloneAliasName, $AliasName)) {
|
|
|
|
|
return [$CloneAliasID, $CloneArtistID, $CloneAliasName, $CloneRedirect];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function resolveRedirect(int $redirectId) {
|
|
|
|
|
/**
|
|
|
|
|
* @param int $redirectId
|
|
|
|
|
* @return int
|
|
|
|
|
* @throws OutOfBoundsException
|
|
|
|
|
* @throws UnexpectedValueException
|
|
|
|
|
*/
|
|
|
|
|
public function resolveRedirect(int $redirectId): int {
|
|
|
|
|
[$foundId, $foundRedirectId] = $this->db->row("
|
|
|
|
|
SELECT ArtistID, Redirect
|
|
|
|
|
FROM artists_alias
|
|
|
|
|
@@ -180,14 +182,20 @@ class Artist extends Base {
|
|
|
|
|
", $redirectId
|
|
|
|
|
);
|
|
|
|
|
if (!$foundId) {
|
|
|
|
|
throw new \Exception("Artist:not-found");
|
|
|
|
|
throw new OutOfBoundsException("Artist:not-found");
|
|
|
|
|
}
|
|
|
|
|
elseif ($this->id != $foundId) {
|
|
|
|
|
throw new \Exception("Artist:not-redirected");
|
|
|
|
|
if ($this->id !== $foundId) {
|
|
|
|
|
throw new UnexpectedValueException("Artist:not-redirected");
|
|
|
|
|
}
|
|
|
|
|
return $foundRedirectId > 0 ? $foundRedirectId : $redirectId;
|
|
|
|
|
return $foundRedirectId > 0 ? (int) $foundRedirectId : $redirectId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param int $userId
|
|
|
|
|
* @param string $name
|
|
|
|
|
* @param int $redirect
|
|
|
|
|
* @return int|void
|
|
|
|
|
*/
|
|
|
|
|
public function addAlias(int $userId, string $name, int $redirect) {
|
|
|
|
|
$this->db->prepared_query("
|
|
|
|
|
INSERT INTO artists_alias
|
|
|
|
|
@@ -198,7 +206,7 @@ class Artist extends Base {
|
|
|
|
|
return $this->db->inserted_id();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function removeAlias(int $aliasId) {
|
|
|
|
|
public function removeAlias(int $aliasId): void {
|
|
|
|
|
$this->db->prepared_query("
|
|
|
|
|
UPDATE artists_alias SET
|
|
|
|
|
ArtistID = ?,
|
|
|
|
|
@@ -208,18 +216,19 @@ class Artist extends Base {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function getAlias($name) {
|
|
|
|
|
public function getAlias($name): int {
|
|
|
|
|
$alias = $this->db->scalar('
|
|
|
|
|
SELECT AliasID
|
|
|
|
|
FROM artists_alias
|
|
|
|
|
WHERE ArtistID = ?
|
|
|
|
|
AND ArtistID != AliasID
|
|
|
|
|
AND Name = ?',
|
|
|
|
|
$this->id, $name);
|
|
|
|
|
AND Name = ?
|
|
|
|
|
', $this->id, $name
|
|
|
|
|
);
|
|
|
|
|
return $alias ?: $this->id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function editableInformation() {
|
|
|
|
|
public function editableInformation(): array {
|
|
|
|
|
return $this->db->row("
|
|
|
|
|
SELECT
|
|
|
|
|
ag.Name,
|
|
|
|
|
@@ -232,10 +241,10 @@ class Artist extends Base {
|
|
|
|
|
LEFT JOIN artist_discogs AS dg ON (dg.artist_id = ag.ArtistID)
|
|
|
|
|
WHERE ag.ArtistID = ?
|
|
|
|
|
", $this->id
|
|
|
|
|
);
|
|
|
|
|
) ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function redirects() {
|
|
|
|
|
public function redirects(): array {
|
|
|
|
|
$this->db->prepared_query("
|
|
|
|
|
SELECT AliasID as aliasId, Name as aliasName, UserID as userId, Redirect as redirectId
|
|
|
|
|
FROM artists_alias
|
|
|
|
|
@@ -245,8 +254,9 @@ class Artist extends Base {
|
|
|
|
|
return $this->db->to_array('aliasId', MYSQLI_ASSOC);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function requests() {
|
|
|
|
|
if (($requests = $this->cache->get_value("artists_requests_" . $this->id)) === false) {
|
|
|
|
|
public function requests(): array {
|
|
|
|
|
$requests = $this->cache->get_value("artists_requests_" . $this->id);
|
|
|
|
|
if (empty($requests)) {
|
|
|
|
|
$this->db->prepared_query('
|
|
|
|
|
SELECT
|
|
|
|
|
r.ID,
|
|
|
|
|
@@ -271,7 +281,7 @@ class Artist extends Base {
|
|
|
|
|
return $requests;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function loadArtistRole() {
|
|
|
|
|
public function loadArtistRole(): int {
|
|
|
|
|
$this->db->prepared_query('
|
|
|
|
|
SELECT ta.GroupID, ta.Importance as artistRole
|
|
|
|
|
FROM torrents_artists AS ta
|
|
|
|
|
@@ -301,24 +311,16 @@ class Artist extends Base {
|
|
|
|
|
return $nr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function hasRole($role) {
|
|
|
|
|
public function hasRole($role): bool {
|
|
|
|
|
return $this->artistRole[$role] > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function activeRoles() {
|
|
|
|
|
return array_filter($this->artistRole, function ($role) {return $role > 0;});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function groupRole($groupId) {
|
|
|
|
|
return isset($this->groupRole[$groupId]) ? $this->groupRole[$groupId] : false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function groupIds() {
|
|
|
|
|
public function groupIds(): array {
|
|
|
|
|
/* this is needed to call \Torrents::get_groups() */
|
|
|
|
|
return $this->groupList;
|
|
|
|
|
return $this->groupList ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function loadGroups($torrentGroupList) {
|
|
|
|
|
public function loadGroups(array $torrentGroupList): int {
|
|
|
|
|
$this->sections = [];
|
|
|
|
|
$this->nrGroups = 0;
|
|
|
|
|
$this->nrLeechers = 0;
|
|
|
|
|
@@ -362,60 +364,61 @@ class Artist extends Base {
|
|
|
|
|
return $this->nrGroups;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function nrGroups() {
|
|
|
|
|
return $this->nrGroups;
|
|
|
|
|
public function nrGroups(): int {
|
|
|
|
|
return $this->nrGroups ?? 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function nrLeechers() {
|
|
|
|
|
return $this->nrLeechers;
|
|
|
|
|
public function nrLeechers(): int {
|
|
|
|
|
return $this->nrLeechers ?? 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function nrSnatches() {
|
|
|
|
|
return $this->nrSnatches;
|
|
|
|
|
public function nrSnatches(): int {
|
|
|
|
|
return $this->nrSnatches ?? 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function nrSeeders() {
|
|
|
|
|
return $this->nrSeeders;
|
|
|
|
|
public function nrSeeders(): int {
|
|
|
|
|
return $this->nrSeeders ?? 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function nrTorrents() {
|
|
|
|
|
return $this->nrTorrents;
|
|
|
|
|
public function nrTorrents(): int {
|
|
|
|
|
return $this->nrTorrents ?? 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function sections() {
|
|
|
|
|
return $this->sections;
|
|
|
|
|
public function sections(): array {
|
|
|
|
|
return $this->sections ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function name() {
|
|
|
|
|
public function name(): string {
|
|
|
|
|
return $this->name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function image() {
|
|
|
|
|
public function image(): ?string {
|
|
|
|
|
return $this->image;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function body() {
|
|
|
|
|
public function body(): ?string {
|
|
|
|
|
return $this->body;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function vanityHouse() {
|
|
|
|
|
public function vanityHouse(): bool {
|
|
|
|
|
return $this->vanity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function similarArtists() {
|
|
|
|
|
public function similarArtists(): array {
|
|
|
|
|
return $this->similar;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function url() {
|
|
|
|
|
return sprintf('<a href="artist.php?id=%d">%s</a>',
|
|
|
|
|
$this->id, $this->name
|
|
|
|
|
);
|
|
|
|
|
public function url(): string {
|
|
|
|
|
return sprintf('<a href="artist.php?id=%d">%s</a>', $this->id, $this->name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function setDiscogsRelation(int $discogsId, int $userId) {
|
|
|
|
|
if ($this->discogsId == $discogsId) {
|
|
|
|
|
/**
|
|
|
|
|
* Sets the Discogs ID for the artist and returns the number of affected rows.
|
|
|
|
|
*/
|
|
|
|
|
public function setDiscogsRelation(int $discogsId, int $userId): int {
|
|
|
|
|
if ($this->discogsId === $discogsId) {
|
|
|
|
|
// don't blindly set the Discogs ID to something else if it's already set, or doesn't change
|
|
|
|
|
return;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
$curl = curl_init();
|
|
|
|
|
|
|
|
|
|
@@ -434,7 +437,7 @@ class Artist extends Base {
|
|
|
|
|
|
|
|
|
|
$result = curl_exec($curl);
|
|
|
|
|
if ($result === false || curl_getinfo($curl, CURLINFO_RESPONSE_CODE) !== 200) {
|
|
|
|
|
return null;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Discogs names are e.g. "Spectrum (4)"
|
|
|
|
|
@@ -446,11 +449,10 @@ class Artist extends Base {
|
|
|
|
|
*/
|
|
|
|
|
$payload = json_decode($result);
|
|
|
|
|
$this->discogsName = $payload->name;
|
|
|
|
|
if (preg_match('/^(.*) \((\d+)\)$/', $this->discogsName, $match) ) {
|
|
|
|
|
if (preg_match('/^(.*) \((\d+)\)$/', $this->discogsName, $match)) {
|
|
|
|
|
$this->discogsStem = $match[1];
|
|
|
|
|
$this->discogsSequence = $match[2];
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
} else {
|
|
|
|
|
$this->discogsStem = $this->discogsName;
|
|
|
|
|
$this->discogsSequence = 1;
|
|
|
|
|
}
|
|
|
|
|
@@ -460,20 +462,21 @@ class Artist extends Base {
|
|
|
|
|
INSERT INTO artist_discogs
|
|
|
|
|
(artist_discogs_id, artist_id, is_preferred, sequence, stem, name, user_id)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
', $this->discogsId, $this->id, $this->homonymCount() == 0, $this->discogsSequence, $this->discogsStem, $this->discogsName, $userId
|
|
|
|
|
', $this->discogsId, $this->id, $this->homonymCount() == 0,
|
|
|
|
|
$this->discogsSequence, $this->discogsStem, $this->discogsName, $userId
|
|
|
|
|
);
|
|
|
|
|
$this->flushCache();
|
|
|
|
|
return $this->db->affected_rows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function homonymCount() {
|
|
|
|
|
public function homonymCount(): int {
|
|
|
|
|
return $this->db->scalar('
|
|
|
|
|
SELECT count(*) FROM artist_discogs WHERE stem = ?
|
|
|
|
|
', $this->discogsStem
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function removeDiscogsRelation() {
|
|
|
|
|
public function removeDiscogsRelation(): int {
|
|
|
|
|
$this->db->prepared_query('
|
|
|
|
|
DELETE FROM artist_discogs WHERE artist_id = ?
|
|
|
|
|
', $this->id
|
|
|
|
|
@@ -482,15 +485,15 @@ class Artist extends Base {
|
|
|
|
|
return $this->db->affected_rows();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function discogsId() {
|
|
|
|
|
public function discogsId(): ?int {
|
|
|
|
|
return $this->discogsId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function discogsName() {
|
|
|
|
|
public function discogsName(): ?string {
|
|
|
|
|
return $this->discogsName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function discogsIsPreferred() {
|
|
|
|
|
public function discogsIsPreferred(): bool {
|
|
|
|
|
return $this->discogsIsPreferred;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -500,9 +503,10 @@ class Artist extends Base {
|
|
|
|
|
* Collapse whitespace and directional markers, because people copypaste carelessly.
|
|
|
|
|
* TODO: make stricter, e.g. on all whitespace characters or Unicode normalisation
|
|
|
|
|
*
|
|
|
|
|
* @param string $ArtistName
|
|
|
|
|
* @param string $name
|
|
|
|
|
* @return string|null
|
|
|
|
|
*/
|
|
|
|
|
public static function sanitize(string $name) {
|
|
|
|
|
public static function sanitize(string $name): ?string {
|
|
|
|
|
// \u200e is ‎
|
|
|
|
|
$name = preg_replace('/^(?:\xE2\x80\x8E|\s)+/', '', $name);
|
|
|
|
|
$name = preg_replace('/(?:\xE2\x80\x8E|\s)+$/', '', $name);
|
|
|
|
|
|