PHP 8.0: Τύποι δεδομένων (2/4)

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

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

Ας επιστρέψουμε στην ιστορία. Η θεμελιώδης πρόοδος της PHP 7 ήταν η εισαγωγή των scalar type hints. Παραλίγο να μην συμβεί. Η δημιουργός της εκπληκτικής λύσης, Andrea Faulds, η οποία χάρη στο declare(strict_types=1) ήταν πλήρως συμβατή προς τα πίσω και προαιρετική, απορρίφθηκε άσχημα από την κοινότητα. Ευτυχώς, τότε την υποστήριξε, μαζί με την πρότασή της, ο Anthony Ferrara, ξεκίνησε μια καμπάνια και το RFC πέρασε οριακά. Ουφ. Τα περισσότερα νέα χαρακτηριστικά στην PHP 8 οφείλονται στον θρυλικό Nikita Popov και πέρασαν στην ψηφοφορία σαν βούτυρο. Ο κόσμος αλλάζει προς το καλύτερο.

Η PHP 8 φέρνει τους τύπους στην τελειότητα. Θα εξαφανιστεί η συντριπτική πλειοψηφία των phpDoc annotations @param, @return και @var στον κώδικα και θα αντικατασταθούν από την εγγενή γραφή και, κυρίως, τον έλεγχο από τη μηχανή της PHP. Στα σχόλια θα παραμείνουν μόνο περιγραφές δομών όπως string[] ή πιο σύνθετες annotations για το PHPStan.

Union Types

Οι Union types είναι μια απαρίθμηση δύο ή περισσότερων τύπων που μπορεί να πάρει μια τιμή:

class Button
{
	private string|object $caption;

	public function setCaption(string|object $caption)
	{
		$this->caption = $caption;
	}
}

Η PHP γνώριζε ήδη κάποιους ειδικούς union types. Για παράδειγμα, οι nullable τύποι όπως ?string, που είναι ισοδύναμο του union type string|null, και η γραφή με ερωτηματικό μπορεί να θεωρηθεί απλώς συντομογραφία. Φυσικά, λειτουργεί και στην PHP 8, αλλά δεν μπορεί να συνδυαστεί με κάθετες γραμμές, οπότε αντί για ?string|object πρέπει να γράψετε το πλήρες string|object|null. Επιπλέον, το iterable ήταν πάντα ισοδύναμο του array|Traversable. Ίσως σας εκπλήξει ότι union type είναι στην πραγματικότητα και το float, το οποίο στην πραγματικότητα δέχεται int|float, αλλά το μετατρέπει σε float.

Στους unions δεν μπορούν να χρησιμοποιηθούν οι ψευδοτύποι void και mixed, επειδή αυτό δεν θα είχε κανένα νόημα.

Η Nette είναι έτοιμη για τύπους ένωσης. Στο Schema, το Expect::from() τους δέχεται, και οι παρουσιαστές τους δέχονται επίσης. Μπορείτε να τους χρησιμοποιήσετε σε μεθόδους render και action, για παράδειγμα:

public function renderDetail(int|array $id)
{
	...
}

Αντίθετα, το autowiring στο Nette DI απορρίπτει τους union types. Δεν υπάρχει ακόμα περίπτωση χρήσης όπου θα είχε νόημα, για παράδειγμα, ένας κατασκευαστής να δέχεται είτε το ένα είτε το άλλο αντικείμενο. Φυσικά, αν εμφανιστεί, θα είναι δυνατό να προσαρμοστεί η συμπεριφορά του container ανάλογα.

Οι μέθοδοι getParameterType(), getReturnType() και getPropertyType() στο Nette\Utils\Reflection προκαλούν εξαίρεση στην περίπτωση union type (στην έκδοση 3.1, στην παλαιότερη 3.0 επιστρέφουν null για λόγους συμβατότητας).

mixed

Ο ψευδοτύπος mixed δηλώνει ότι η τιμή μπορεί να είναι οτιδήποτε.

Στην περίπτωση παραμέτρων και properties, πρόκειται στην πραγματικότητα για την ίδια συμπεριφορά όπως όταν δεν δηλώνουμε κανέναν τύπο. Σε τι χρησιμεύει λοιπόν; Για να μπορούμε να διακρίνουμε πότε ο τύπος απλώς λείπει και πότε είναι πραγματικά mixed.

Στην περίπτωση της τιμής επιστροφής συνάρτησης και μεθόδου, η μη δήλωση τύπου διαφέρει από τη δήλωση τύπου mixed. Είναι στην πραγματικότητα το αντίθετο του void, καθώς δηλώνει ότι η συνάρτηση πρέπει να επιστρέψει κάτι. Η απουσία return είναι τότε μοιραίο σφάλμα.

Στην πράξη, θα πρέπει να το χρησιμοποιείτε σπάνια, επειδή χάρη στους union types μπορείτε να καθορίσετε την τιμή με μεγαλύτερη ακρίβεια. Είναι λοιπόν χρήσιμο σε εξαιρετικές περιπτώσεις:

function dump(mixed $var): mixed
{
	// εκτύπωση μεταβλητής
	return $var;
}

false

Ο νέος ψευδοτύπος false μπορεί, αντίθετα, να χρησιμοποιηθεί μόνο σε union types. Δημιουργήθηκε από την ανάγκη να περιγραφεί εγγενώς ο τύπος της τιμής επιστροφής σε εγγενείς συναρτήσεις, οι οποίες ιστορικά επιστρέφουν false σε περίπτωση αποτυχίας:

function strpos(string $haystack, string $needle): int|false
{
}

Για το λόγο αυτό, δεν υπάρχει τύπος true, ούτε μπορεί να χρησιμοποιηθεί μόνο το false ή το false|null ή το bool|false.

static

Ο ψευδοτύπος static μπορεί να χρησιμοποιηθεί μόνο ως τύπος επιστροφής μεθόδου. Δηλώνει ότι η μέθοδος επιστρέφει ένα αντικείμενο του ίδιου τύπου με το ίδιο το αντικείμενο (ενώ το self δηλώνει ότι επιστρέφει την κλάση στην οποία ορίζεται η μέθοδος). Αυτό είναι εξαιρετικά χρήσιμο για την περιγραφή fluent interfaces:

class Item
{
	public function setValue($val): static
	{
		$this->value = $val;
		return $this;
	}
}

class ItemChild extends Item
{
	public function childMethod()
	{
	}
}

$child = new ItemChild;
$child->setValue(10)
	->childMethod();

resource

Αυτός ο τύπος δεν υπάρχει στην PHP 8 και ούτε θα εισαχθεί στο μέλλον. Οι Resources είναι ένα ιστορικό κατάλοιπο από την εποχή που η PHP δεν είχε ακόμη αντικείμενα. Σταδιακά, οι resources θα αντικατασταθούν από αντικείμενα και με τον καιρό αυτός ο τύπος θα εξαφανιστεί εντελώς. Για παράδειγμα, η PHP 8.0 αντικαθιστά τον resource που αντιπροσωπεύει μια εικόνα με το αντικείμενο GdImage και τον resource σύνδεσης curl με το αντικείμενο CurlHandle.

Stringable

Πρόκειται για ένα interface που υλοποιείται αυτόματα από κάθε αντικείμενο με τη μαγική μέθοδο __toString().

class Email
{
	public function __toString(): string
	{
		return $this->value;
	}
}

function print(Stringable|string $s)
{
}

print('abc');
print(new Email);

Στον ορισμό της κλάσης, είναι δυνατό να δηλωθεί ρητά class Email implements Stringable, αλλά δεν είναι απαραίτητο.

Αυτό το στυλ ονομασίας αντικατοπτρίζεται και στο Nette\Utils\Html, το οποίο υλοποιεί το interface Nette\HtmlStringable αντί του προηγούμενου IHtmlString. Αντικείμενα αυτού του τύπου, για παράδειγμα, δεν διαφεύγουν (escape) από το Latte.

Type variance, contravariance, covariance

Η Αρχή Αντικατάστασης Liskov (Liskov Substitution Principle – LSP) λέει ότι οι απόγονοι μιας κλάσης και οι υλοποιήσεις ενός interface δεν πρέπει ποτέ να απαιτούν περισσότερα και να παρέχουν λιγότερα από τον γονέα. Δηλαδή, η μέθοδος ενός απογόνου δεν πρέπει να απαιτεί περισσότερα ορίσματα ή να δέχεται στις παραμέτρους ένα στενότερο εύρος τύπων από τον πρόγονο και, αντίθετα, δεν πρέπει να επιστρέφει ένα ευρύτερο εύρος τύπων. Αλλά μπορεί να επιστρέφει ένα στενότερο. Γιατί; Επειδή διαφορετικά η κληρονομικότητα δεν θα λειτουργούσε καθόλου. Μια συνάρτηση θα δεχόταν μεν ένα αντικείμενο συγκεκριμένου τύπου, αλλά δεν θα γνώριζε ποιες παραμέτρους μπορούν να περαστούν στις μεθόδους και τι θα επιστρέψουν πραγματικά, επειδή οποιοσδήποτε απόγονος θα μπορούσε να το παραβιάσει.

Έτσι, στο OOP ισχύει ότι ο απόγονος μπορεί:

  • στις παραμέτρους να δέχεται ευρύτερο εύρος τύπων (αυτό ονομάζεται contravariance)
  • να επιστρέφει στενότερο εύρος τύπων (covariance)
  • και οι properties δεν μπορούν να αλλάξουν τύπο (είναι invariant)

Η PHP το υποστηρίζει αυτό από την έκδοση 7.4 και όλοι οι νέοι τύποι που εισήχθησαν στην PHP 8.0 υποστηρίζουν επίσης contravariance και covariance.

Στην περίπτωση του mixed, ο απόγονος μπορεί να περιορίσει την τιμή επιστροφής σε οποιονδήποτε τύπο, όχι όμως στο void, επειδή δεν πρόκειται για τύπο τιμής, αλλά για την απουσία της. Ούτε ο απόγονος μπορεί να μην δηλώσει τύπο, επειδή και αυτό επιτρέπει την απουσία.

class A
{
    public function foo(mixed $foo): mixed
    {}
}

class B extends A
{
    public function foo($foo): string
    {}
}

Επίσης, οι union types μπορούν στις παραμέτρους να επεκταθούν και στις τιμές επιστροφής να περιοριστούν:

class A
{
    public function foo(string|int $foo): string|int
    {}
}

class B extends A
{
    public function foo(string|int|float $foo): string
    {}
}

Επιπλέον, το false μπορεί στην παράμετρο να επεκταθεί σε bool ή αντίθετα το bool στην τιμή επιστροφής να περιοριστεί σε false.

Όλες οι παραβάσεις της covariance/contravariance οδηγούν στην PHP 8.0 σε fatal error.

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

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