
Partager:
Acteur de formation avec une thèse sur la comédie, je suis venu au développement PHP par le biais de la scène des rencontres. Vous pouvez me trouver en train de parler et d'écrire sur la technologie, ou de jouer/acheter des disques bizarres de ma collection de vinyles.
Type Safety Done Right - PHP Array Hacking (en anglais)
Temps de lecture : 6 minutes
Au cours des cinq ou six premières années de mon expérience dans le développement d'applications PHP à grande échelle, j'ai expérimenté et créé de nombreux problèmes que les personnes qui débutent dans le développement de backend auront souvent vus. Ce n'est que maintenant, surtout après avoir hérité du Vonage PHP SDKde Vonage, que j'ai réalisé que j'étais en mesure d'apprendre aux autres à ne pas faire la même chose. Ces problèmes proviennent en fait de la façon dont j'ai vu PHP en particulier écrit au fil des ans, indépendamment de l'utilisation ou non d'un framework. Lançons la première grenade :
Les tableaux en PHP sont en quelque sorte horribles
D'accord, ils ne sont pas terribles, ce serait une chose étrange à dire. Ce que je veux dire, c'est que la façon dont ils sont souvent utilisés est affreuse. Il y a une très bonne raison à cela ; en commençant par PHP comme premier langage (après VBA et SQL), je ne savais pas comment les autres langages géraient les collections de données. J'ai simplement supposé qu'ils étaient identiques à PHP :
Vous disposez d'un tableau d'index, qui comporte une clé numérique ou
Vous disposez d'un tableau associé, dont les clés alphanumériques sont définies.
Les deux sont considérés comme le même type de variable, c'est-à-dire arraycar le moteur a déterminé le type de tableau que vous souhaitez créer.
$myAssociatedArray = ['foo' => 'bar']; // array
$myHashedArray = ['foo', 'bar']; // also array
C'est ainsi que tout le reste fonctionne probablement, n'est-ce pas ?
Non, ce n'est pas le cas. Ma naïveté dans l'utilisation de PHP m'a fait ignorer l'affirmation suivante :
Dans tous les autres grands langages de gestion, les tableaux indexés et associatifs sont divisés en deux ou plusieurs classes de base différentes.
De plus, les tableaux associatifs sont un terme utilisé uniquement en PHP. A partir de maintenant, nous appellerons tableaux associatifs tableaux hachés car c'est le terme le plus utilisé ailleurs.
Voici le comportement pour les autres langues :
NodeJS
Array(tableau indexé)Object(tableau haché)
Python
List(tableau indexé)Dictionary(tableau haché), en utilisant List Comprehension
Rubis
Array(tableau indexé)Hash(tableau haché)
Il est intéressant de noter que Ruby peut avoir un tableau associatif de la même manière que PHP, mais en raison de l'existence de l'objet Hash, il est très rare de voir un tableau utilisé comme un hachage.
Go(lang)
Slice(tableau indexé)Map(tableau haché)
Java
Array(tableau indexé, primitif)List(interface, mise en œuvre parArrayList)ArrayList(tableau indexé, crée un nouvel objet pour le dimensionnement)Hashmap(tableau haché, mise en œuvre deMap)LinkedHashMap(tableau haché, retrace l'ordre d'insertion)
Il convient de noter que, comme vous pouvez le constater, Java est très strict en ce qui concerne les ensembles de structures de données. Toutes ces structures sont mutables en ce sens que vous pouvez modifier leurs valeurs, mais vous ne pouvez pas modifier la taille de l'objet par rapport à ses nœuds. C'est pourquoi vous pouvez dans ArrayListparce qu'il crée un nouvel objet lors de la modification.
C#
Array(tableau indexé)List(tableau indexé avec génériques, mutable)Dictionary(tableau haché)
Ainsi, étant donné que toutes ces classes différentes font partie de l'API de base de ces langages, cela signifie qu'ils ont tous la possibilité d'instaurer un comportement plus strict en matière de types.
Et alors ?
Nous n'avons pas cela en PHP. Cela signifie que les tableaux Les tableaux en PHP peuvent être en quelque sorte n'importe quoi. C'est ce fait qui est à l'origine du type de code que je lis depuis des années et qui ressemble à ceci :
public function updateRecord(int $id, array $options) {
// do some stuff here
}Je veux mettre à jour l'ID de quelque chose. Et puis... J'ai $options. Parce que ce sont des options. Mais c'est un simple tableau PHP. Qu'est-ce qu'il contient ? La fonction n'en a aucune idée.
Qu'est-ce qui ne va pas avec ce code ? Si l'on se fie à l'expérience personnelle, quatre heures de xdebug pour savoir d'où vient une clé de tableau. Nous n'avons aucune idée de ce à quoi il est censé ressembler. Cette particulier est quelque chose que j'ai codé moi-même pendant des années, surtout dans les agences où le temps est de l'argent. Si la seule préoccupation est "Est-ce que cela fonctionne selon les exigences du client ?", alors on coupera les coins ronds autant que possible pour livrer à temps. La personne qui, après vous, doit corriger un bogue causé par un effet secondaire ? C'est la dette technologique en action.
OK, et maintenant ?
Le chemin que j'ai emprunté pour obtenir un code facile à maintenir est d'utiliser des indications de type partout, tout le temps. C'est le type de développement de logiciel qui, s'il est effectué de manière complète et minutieuse, devrait aboutir au mythique code auto-documenté. Mais nous ne pouvons taper que hint an arrayce qui ne nous aide pas. Nous avons donc trois options :
Génériques
Forme du tableau (via. hacked Generics)
Objets de valeur
Génériques
Désolé, je vous ai piégé : nous n'avons pas de génériques en PHP à cause des limitations du moteur. Cependant, comme vous pouvez voir le mot un peu partout, considérez le code suivant de TypeScript:
interface UserProfile {
id: string;
name: string;
age: integer;
}
class Collection<Type> {
private items: Type[] = [];
add(item: Type) {
this.items.push(item);
}
get(index: number): Type | undefined {
return this.items[index];
}
}
const userProfiles = new Collection<UserProfile>();
Pour quelqu'un qui est tout à fait dans le camp du "rendez ce PHP aussi robuste que possible, rendez-le plus dur que l'acier Valyrian", les génériques comme celui-ci sont une joie profonde de travailler avec. Les <> indique que lors de la création d'un nouvel objet Collection nouvel objet, il doit contenir que le type donné lors de son instanciation. Ainsi, lorsque nous créons un nouvel objet Collection<UserProfile>nous disons que cet objet ne peut contenir que des objets de type UserProfile objets. Ce codeamis et ennemis, est un code auto-documenté.
PHP peut-il faire autrement ?
Je suis heureux que vous posiez la question, oui, c'est possible ! Cela nous amène au point numéro deux, qui est la définition d'un tableau. Cela semble tellement stupide de dire cela, mais jusqu'à ce que je rejoigne Vonage, il ne m'était jamais venu à l'esprit que je pouvais étendre les fonctionnalités de core SPL dans l'API PHP. Donc, vous pouvez étendre les tableaux!
class Collection extends \ArrayObject {
protected function __construct(...$args) {
parent::__construct(...args);
}
}Actuellement, cela ne fait rien d'autre que de passer les arguments à la classe mère avec l'opérateur splat, qui est un opérateur ArrayObject. Mais nous l'avons étendu, ce qui signifie que nous pouvons surcharger le comportement !
Nous devons faire deux choses :
Définir un type, de la même manière qu'en TypeScript
Surchargez la méthode
offsetSet()qui est la méthode SPL pour ajouter à un tableau (soit en ajoutant une valeur après l'instanciation, soit pendant la création du constructeur).
Alors, abordons cette question :
class Collection extends \ArrayObject {
protected $typeError = 'Only UserProfile objects can be added!'
public function __construct(...$args) {
foreach ($args as $arg) {
if (!$arg instanceof UserProfile) {
throw new \TypeError($this->typeError);
}
}
parent::__construct(...$args);
}
public function offsetSet($key, $value): void
{
if (!$value instanceof UserProfile) {
throw new \TypeError($this->typeError);
}
parent::offsetSet($key, $value);
}
}
Et voilà. offsetSet est surchargé de façon à ce que seuls les objets UserProfile puissent être ajoutés, et lors de la création de l'objet Collection vous pouvez passer n'importe quel nombre d'arguments tant qu'il s'agit d'objets. UserProfile objets.
Objets de valeur
Il existe une solution beaucoup plus simple au problème de l'utilisation des tableaux : ne pas les utiliser. J'ai poursuivi le travail effectué par mes prédécesseurs sur le SDK PHP Core de Vonage pour m'assurer que tout ce qui est transmis aux méthodes du client est un objet de valeur.
Oui, il y a des inconvénients à cela. Cela signifie que les propriétés de la classe doivent être définies, que les méthodes getter et setter doivent être ajoutées. Cela signifie plus de code. Cependant, nous disposons également d'outils pour rendre le code moins volumineux en PHP moderne, avec l'utilisation soit de classes natives, soit de la promotion des propriétés des constructeurs, avec des modificateurs d'accès. enum ou de la promotion des propriétés des constructeurs, avec des modificateurs d'accès pour réduire le nombre de lignes de code.
Un bon exemple de cela a été lorsque j'ai codé l'implémentation du SDK PHP de Verify v2. L'envoi d'une demande de vérification ressemblerait à ceci sans les objets de valeur :
$payload = [
'locale' => 'en_us',
'channel_timeout' => 300,
'client_ref' => 'a-reference',
'code_length' => 4,
'workflow' => [
'channel' => 'sms',
'to' => '123456789'
]
];
$myVonageClient = new Client('apiKey', 'apiSecret');
$myVonageClient->verify2()->startVerification($payload);
L'objet client ne sait rien du tableau qui lui est transmis. Que se passe-t-il si une clé est erronée ? Qu'en est-il si une clé est manquante ? Il n'y a rien dans le code qui vous renseigne sur le comportement : et si je faisais mettre la validation dans la méthode startVerification() il faudrait alors parcourir mon code pour savoir ce que j'avais en tête.
Au lieu de cela, en utilisant des objets de valeur, nous déplaçons la logique dans un PHP bien typé. Il devient auto-documenté et renforcé par conception.
class SMSRequest extends BaseVerifyRequest
{
public function __construct(
protected string $to,
protected string $brand,
protected ?VerificationLocale $locale = null,
) {
if (!$this->locale) {
$this->locale = new VerificationLocale();
}
$workflow = new VerificationWorkflow(VerificationWorkflow::WORKFLOW_SMS, $to);
$this->addWorkflow($workflow);
}
}
Cet objet est maintenant transmis à la méthode startVerification() . Ce que vous pouvez et ne pouvez pas faire dans les objets passés peut maintenant être défini au niveau le plus bas du code. Par exemple, voici l'objet VerificationLocale objet.
class VerificationLocale
{
private array $allowedCodes = [
'en-us',
'en-gb',
'es-es',
'es-mx',
'es-us',
'it-it',
'fr-fr',
'de-de',
'ru-ru',
'hi-in',
'pt-br',
'pt-pt',
'id-id',
];
public function __construct(protected string $code = 'en-us')
{
if (! in_array($code, $this->allowedCodes, true)) {
throw new \InvalidArgumentException('Invalid Locale Code Provided');
}
}
public function getCode(): string
{
return $this->code;
}
public function setCode(string $code): static
{
$this->code = $code;
return $this;
}
}
Et voilà. Un code auto-documenté. Oui, vous devez créer tous les objets pour les assembler, mais le comportement de notre SDK est à la fois strict et explicite.
Partager:
Acteur de formation avec une thèse sur la comédie, je suis venu au développement PHP par le biais de la scène des rencontres. Vous pouvez me trouver en train de parler et d'écrire sur la technologie, ou de jouer/acheter des disques bizarres de ma collection de vinyles.