PHP 8.0: Attributes (3/4)

πριν από 4 χρόνια Από David Grudl  

Κυκλοφόρησε η έκδοση 8.0 της PHP. Είναι τόσο γεμάτη με νέα χαρακτηριστικά όσο καμία προηγούμενη έκδοση. Η παρουσίασή τους απαιτεί τέσσερα ξεχωριστά άρθρα. Σε αυτό το τρίτο, θα εξετάσουμε τα attributes.

Τα attributes φέρνουν έναν εντελώς νέο τρόπο για τη γραφή δομημένων μεταδεδομένων σε κλάσεις και όλα τα μέλη τους, καθώς και σε συναρτήσεις, closures και τις παραμέτρους τους. Για το σκοπό αυτό, μέχρι τώρα χρησιμοποιούνταν τα phpDoc σχόλια, αλλά η σύνταξή τους ήταν πάντα τόσο ελεύθερη και ανομοιόμορφη που δεν ήταν δυνατό να αρχίσουν να επεξεργάζονται μηχανικά. Γι' αυτό αντικαθίστανται από τα attributes με σταθερή σύνταξη και υποστήριξη στις κλάσεις reflection.

Έτσι, οι βιβλιοθήκες που μέχρι τώρα αποκτούσαν μεταδεδομένα αναλύοντας τα phpDoc σχόλια, θα μπορούν να τα αντικαταστήσουν με attributes. Παράδειγμα είναι το Nette, όπου στις τελευταίες εκδόσεις των Application και DI μπορείτε ήδη να χρησιμοποιείτε τα attributes Persistent, CrossOrigin και Inject αντί για τις annotations @persistent, @crossOrigin και @inject.

Κώδικας που χρησιμοποιεί annotations:

/**
 * @persistent(comp1, comp2)
 */
class SomePresenter
{
	/** @persistent */
	public $page = 1;

	/** @inject */
	public Facade $facade;

	/**
	 * @crossOrigin
	 */
	public function handleSomething()
	{
	}
}

Το ίδιο με attributes:

use Nette\Application\Attributes\CrossOrigin;
use Nette\Application\Attributes\Persistent;
use Nette\DI\Attributes\Inject;

#[Persistent('comp1', 'comp2')]
class SomePresenter
{
	#[Persistent]
	public int $page = 1;

	#[Inject]
	public Facade $facade;

	#[CrossOrigin]
	public function handleSomething()
	{
	}
}

Η PHP αξιολογεί τα ονόματα των attributes με τον ίδιο τρόπο σαν να ήταν κλάσεις, δηλαδή στο πλαίσιο του namespace και των ρητρών use. Έτσι, θα μπορούσαν να γραφτούν, για παράδειγμα, και ως εξής:

use Nette\Application\Attributes;

class SomePresenter
{
	#[Attributes\Persistent]
	public int $page = 1;

	#[\Nette\DI\Attributes\Inject]
	public Facade $facade;

Η κλάση που αντιπροσωπεύει ένα attribute μπορεί να υπάρχει ή και να μην υπάρχει. Είναι όμως σίγουρα καλύτερο να υπάρχει, γιατί τότε ο editor μπορεί να την προτείνει κατά τη γραφή, ο στατικός αναλυτής αναγνωρίζει τυπογραφικά λάθη κ.λπ.

Σύνταξη

Είναι βολικό το ότι η PHP πριν την έκδοση 8 βλέπει τα attributes απλώς ως σχόλια, οπότε μπορούν να χρησιμοποιηθούν και σε κώδικα που πρέπει να λειτουργεί σε παλαιότερες εκδόσεις.

Η γραφή ενός μεμονωμένου attribute μοιάζει με τη δημιουργία μιας περίπτωσης αντικειμένου, αν παραλείψουμε τον τελεστή new. Δηλαδή, το όνομα της κλάσης ακολουθούμενο από ορίσματα σε παρενθέσεις:

#[Column('string', 32, true, false)]#
protected $username;

Και εδώ είναι το σημείο όπου μπορεί να χρησιμοποιηθεί το νέο καυτό χαρακτηριστικό της PHP 8.0 – τα ονομαστικά ορίσματα:

#[Column(
	type: 'string',
	length: 32,
	unique: true,
	nullable: false,
)]#
protected $username;

Κάθε στοιχείο μπορεί να έχει πολλαπλά attributes, τα οποία μπορούν να γραφτούν ξεχωριστά ή διαχωρισμένα με κόμμα:

#[Inject]
#[Lazy]
public Foo $foo;

#[Inject, Lazy]
public Bar $bar;

Το παρακάτω attribute ισχύει και για τις τρεις properties:

#[Common]
private $a, $b, $c;

Στις προεπιλεγμένες τιμές των properties μπορούν να χρησιμοποιηθούν απλές εκφράσεις και σταθερές, οι οποίες μπορούν να αξιολογηθούν κατά τη μεταγλώττιση, και το ίδιο ισχύει και για τα ορίσματα των attributes:

#[
	ScalarExpression(1 + 1),
	ClassNameAndConstants(PDO::class, PHP_VERSION_ID),
	BitShift(4 >> 1, 4 << 1),
	BitLogic(1 | 2, JSON_HEX_TAG | JSON_HEX_APOS),
]

Δυστυχώς, η τιμή ενός ορίσματος δεν μπορεί να είναι ένα άλλο attribute, δηλαδή δεν μπορούν να ενσωματωθούν attributes. Για παράδειγμα, η ακόλουθη annotation που χρησιμοποιείται στο Doctrine δεν μπορεί να μετατραπεί εντελώς άμεσα σε attributes:

/**
 * @Table(name="products",uniqueConstraints={@UniqueConstraint(columns={"name", "email"})})
 */

Επίσης, δεν υπάρχει αντίστοιχο attribute για το file-level phpDoc, δηλαδή το σχόλιο που βρίσκεται στην αρχή του αρχείου, το οποίο χρησιμοποιείται για παράδειγμα από το Nette Tester.

Ανάκτηση attributes

Ποια attributes έχουν τα μεμονωμένα στοιχεία το μαθαίνουμε μέσω reflection. Οι κλάσεις reflection διαθέτουν μια νέα μέθοδο getAttributes(), η οποία επιστρέφει έναν πίνακα αντικειμένων ReflectionAttribute.

use MyAttributes\Example;

#[Example('Hello', 123)]
class Foo
{}

$reflection = new ReflectionClass(Foo::class);

foreach ($reflection->getAttributes() as $attribute) {
	$attribute->getName();      // πλήρες όνομα του attribute, π.χ. MyAttributes\Example
	$attribute->getArguments(); // ['Hello', 123]
	$attribute->newInstance();  // επιστρέφει την περίπτωση new MyAttributes\Example('Hello', 123)
}

Τα επιστρεφόμενα attributes μπορούν να φιλτραριστούν με μια παράμετρο, π.χ. το $reflection->getAttributes(Example::class) επιστρέφει μόνο τα attributes Example.

Κλάσεις attribute

Η κλάση του attribute MyAttributes\Example δεν χρειάζεται να υπάρχει. Η ύπαρξη απαιτείται μόνο από την κλήση της μεθόδου newInstance() επειδή δημιουργεί την περίπτωσή της. Ας τη γράψουμε λοιπόν. Θα είναι μια εντελώς συνηθισμένη κλάση, απλά πρέπει να δηλώσουμε σε αυτήν το attribute Attribute (δηλαδή από το καθολικό namespace του συστήματος):

namespace MyAttributes;

use Attribute;

#[Attribute]
class Example
{
	public function __construct(string $message, int $number)
	{
		...
	}
}

Μπορεί να περιοριστεί σε ποια γλωσσικά στοιχεία θα είναι νόμιμη η χρήση του attribute. Έτσι, για παράδειγμα, μόνο σε κλάσεις και properties:

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class Example
{
	...
}

Διαθέσιμα είναι τα flags TARGET_CLASS, TARGET_FUNCTION, TARGET_METHOD, TARGET_PROPERTY, TARGET_CLASS_CONSTANT, TARGET_PARAMETER και το προεπιλεγμένο TARGET_ALL.

Αλλά προσοχή, η επαλήθευση της σωστής ή λανθασμένης χρήσης γίνεται εκπληκτικά μόνο κατά την κλήση της μεθόδου newInstance(). Ο ίδιος ο compiler δεν εκτελεί αυτόν τον έλεγχο.

Το μέλλον με τα attributes

Χάρη στα attributes και τους νέους τύπους, τα σχόλια τεκμηρίωσης της PHP θα γίνουν για πρώτη φορά στην ιστορία τους πραγματικά μόνο σχόλια τεκμηρίωσης. Ήδη το PhpStorm έρχεται με δικά του attributes, τα οποία μπορούν να αντικαταστήσουν, για παράδειγμα, την annotation @deprecated. Και μπορεί να υποτεθεί ότι αυτό το attribute θα είναι κάποτε στάνταρ στην PHP. Παρομοίως, θα αντικατασταθούν και άλλες annotations, όπως @throws κ.λπ.

Αν και το Nette χρησιμοποιεί annotations από την πρώτη του έκδοση για τη σήμανση persistent παραμέτρων και components, δεν υπήρξε μαζικότερη χρήση τους επειδή δεν ήταν εγγενής γλωσσική κατασκευή, οπότε οι editors δεν τις πρότειναν και ήταν εύκολο να γίνει λάθος σε αυτές. Αυτό σήμερα λύνεται με plugins για editors, αλλά ο πραγματικά εγγενής τρόπος που φέρνουν τα attributes ανοίγει εντελώς νέες δυνατότητες.

Παρεμπιπτόντως, τα attributes απέκτησαν εξαίρεση στο Nette Coding Standard, το οποίο απαιτεί το όνομα της κλάσης, εκτός από την εξειδίκευση (π.χ. Product, InvalidValue), να περιέχει και τη γενικότητα (δηλαδή ProductPresenter, InvalidValueException). Διαφορετικά, δεν θα ήταν σαφές κατά τη χρήση στον κώδικα τι ακριβώς αντιπροσωπεύει η κλάση. Στα attributes, αυτό αντίθετα δεν είναι επιθυμητό, οπότε η κλάση ονομάζεται Inject αντί για InjectAttribute.

Στο τελευταίο μέρος θα δούμε ποιες νέες συναρτήσεις και κλάσεις εμφανίστηκαν στην PHP και θα παρουσιάσουμε τον Just in Time Compiler.

Πρόσφατες δημοσιεύσεις