SmartObject

Το SmartObject βελτίωνε τη συμπεριφορά των αντικειμένων στην PHP για χρόνια. Από την έκδοση PHP 8.4, όλες οι λειτουργίες του είναι πλέον μέρος της ίδιας της PHP, ολοκληρώνοντας έτσι την ιστορική του αποστολή να είναι πρωτοπόρος της σύγχρονης αντικειμενοστρεφούς προσέγγισης στην PHP.

Εγκατάσταση:

composer require nette/utils

Το SmartObject δημιουργήθηκε το 2007 ως μια επαναστατική λύση στις ελλείψεις του τότε αντικειμενοστρεφούς μοντέλου της PHP. Σε μια εποχή που η PHP υπέφερε από πολλά προβλήματα με τον αντικειμενοστρεφή σχεδιασμό, έφερε σημαντική βελτίωση και απλοποίηση της εργασίας για τους προγραμματιστές. Έγινε ένα θρυλικό μέρος του Nette framework. Προσέφερε λειτουργικότητα που η PHP απέκτησε πολλά χρόνια αργότερα – από τον έλεγχο πρόσβασης στις ιδιότητες των αντικειμένων έως τα εξελιγμένα συντακτικά ζαχαρωτά. Με την έλευση της PHP 8.4, ολοκλήρωσε την ιστορική του αποστολή, καθώς όλες οι λειτουργίες του έγιναν εγγενές μέρος της γλώσσας. Προηγήθηκε της εξέλιξης της PHP κατά αξιοσημείωτα 17 χρόνια.

Τεχνικά, το SmartObject πέρασε από μια ενδιαφέρουσα εξέλιξη. Αρχικά υλοποιήθηκε ως κλάση Nette\Object, από την οποία άλλες κλάσεις κληρονομούσαν την απαραίτητη λειτουργικότητα. Μια θεμελιώδης αλλαγή ήρθε με την PHP 5.4, η οποία έφερε υποστήριξη για traits. Αυτό επέτρεψε τη μετατροπή του σε μορφή trait Nette\SmartObject, η οποία έφερε μεγαλύτερη ευελιξία – οι προγραμματιστές μπορούσαν να χρησιμοποιήσουν τη λειτουργικότητα ακόμη και σε κλάσεις που ήδη κληρονομούσαν από άλλη κλάση. Ενώ η αρχική κλάση Nette\Object εξαφανίστηκε με την έλευση της PHP 7.2 (η οποία απαγόρευσε την ονομασία κλάσεων με τη λέξη Object), το trait Nette\SmartObject ζει ακόμα.

Ας δούμε τα χαρακτηριστικά που κάποτε προσέφεραν το Nette\Object και αργότερα το Nette\SmartObject. Κάθε μία από αυτές τις λειτουργίες στην εποχή της αντιπροσώπευε ένα σημαντικό βήμα προς τα εμπρός στον τομέα του αντικειμενοστρεφούς προγραμματισμού στην PHP.

Συνεπείς καταστάσεις σφάλματος

Ένα από τα πιο πιεστικά προβλήματα της πρώιμης PHP ήταν η ασυνεπής συμπεριφορά κατά την εργασία με αντικείμενα. Το Nette\Object έφερε τάξη και προβλεψιμότητα σε αυτό το χάος. Ας δούμε πώς έμοιαζε η αρχική συμπεριφορά της PHP:

echo $obj->undeclared;    // E_NOTICE, αργότερα E_WARNING
$obj->undeclared = 1;     // περνάει σιωπηλά χωρίς αναφορά
$obj->unknownMethod();    // Fatal error (μη συλλαμβανόμενο με try/catch)

Το Fatal error τερμάτιζε την εφαρμογή χωρίς καμία δυνατότητα αντίδρασης. Η σιωπηλή εγγραφή σε ανύπαρκτα μέλη χωρίς προειδοποίηση μπορούσε να οδηγήσει σε σοβαρά σφάλματα που ήταν δύσκολο να εντοπιστούν. Το Nette\Object συνέλαβε όλες αυτές τις περιπτώσεις και δημιούργησε την εξαίρεση MemberAccessException, η οποία επέτρεψε στους προγραμματιστές να αντιδράσουν στα σφάλματα και να τα επιλύσουν.

echo $obj->undeclared;   // δημιουργεί Nette\MemberAccessException
$obj->undeclared = 1;    // δημιουργεί Nette\MemberAccessException
$obj->unknownMethod();   // δημιουργεί Nette\MemberAccessException

Από την PHP 7.0, η γλώσσα δεν προκαλεί πλέον μη συλλαμβανόμενα fatal errors και από την PHP 8.2, η πρόσβαση σε μη δηλωμένα μέλη θεωρείται σφάλμα.

Βοήθεια “Μήπως εννοούσατε;”

Το Nette\Object ήρθε με μια πολύ ευχάριστη λειτουργία: έξυπνη βοήθεια για τυπογραφικά λάθη. Όταν ένας προγραμματιστής έκανε λάθος στο όνομα μιας μεθόδου ή μεταβλητής, όχι μόνο ανέφερε το σφάλμα, αλλά προσέφερε επίσης ένα χέρι βοήθειας με τη μορφή πρότασης του σωστού ονόματος. Αυτό το εικονικό μήνυμα, γνωστό ως “did you mean?”, εξοικονόμησε ώρες αναζήτησης τυπογραφικών λαθών στους προγραμματιστές:

class Foo extends Nette\Object
{
	public static function from($var)
	{
	}
}

$foo = Foo::form($var);
// δημιουργεί Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"

Η σημερινή PHP δεν έχει καμία μορφή “did you mean?”, αλλά αυτό το συμπλήρωμα μπορεί να προστεθεί στα σφάλματα από το Tracy. Και μπορεί ακόμη και να διορθώσει αυτόματα τέτοια σφάλματα.

Ιδιότητες με ελεγχόμενη πρόσβαση

Μια σημαντική καινοτομία που έφερε το SmartObject στην PHP ήταν οι ιδιότητες με ελεγχόμενη πρόσβαση. Αυτή η έννοια, κοινή σε γλώσσες όπως η C# ή η Python, επέτρεψε στους προγραμματιστές να ελέγχουν κομψά την πρόσβαση στα δεδομένα του αντικειμένου και να διασφαλίζουν τη συνέπειά τους. Οι ιδιότητες είναι ένα ισχυρό εργαλείο του αντικειμενοστρεφούς προγραμματισμού. Λειτουργούν σαν μεταβλητές, αλλά στην πραγματικότητα αντιπροσωπεύονται από μεθόδους (getters και setters). Αυτό επιτρέπει την επικύρωση των εισόδων ή τη δημιουργία τιμών μόνο τη στιγμή της ανάγνωσης.

Για να χρησιμοποιήσετε ιδιότητες πρέπει:

  • Να προσθέσετε στην κλάση μια annotation της μορφής @property <type> $xyz
  • Να δημιουργήσετε έναν getter με το όνομα getXyz() ή isXyz(), έναν setter με το όνομα setXyz()
  • Να διασφαλίσετε ότι ο getter και ο setter είναι public ή protected. Είναι προαιρετικοί – μπορούν δηλαδή να υπάρχουν ως ιδιότητα read-only ή write-only

Ας δούμε ένα πρακτικό παράδειγμα στην κλάση Circle, όπου θα χρησιμοποιήσουμε τις ιδιότητες για να διασφαλίσουμε ότι η ακτίνα θα είναι πάντα ένας μη αρνητικός αριθμός. Αντικαθιστούμε το αρχικό public $radius με μια ιδιότητα:

/**
 * @property float $radius
 * @property-read bool $visible
 */
class Circle
{
	use Nette\SmartObject;

	private float $radius = 0.0; // δεν είναι public!

	// getter για την ιδιότητα $radius
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter για την ιδιότητα $radius
	protected function setRadius(float $radius): void
	{
		// απολυμαίνουμε την τιμή πριν την αποθήκευση
		$this->radius = max(0.0, $radius);
	}

	// getter για την ιδιότητα $visible
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10;  // στην πραγματικότητα καλεί setRadius(10)
echo $circle->radius;  // καλεί getRadius()
echo $circle->visible; // καλεί isVisible()

Από την PHP 8.4, μπορεί να επιτευχθεί η ίδια λειτουργικότητα χρησιμοποιώντας property hooks, τα οποία προσφέρουν πολύ πιο κομψή και συνοπτική σύνταξη:

class Circle
{
	public float $radius = 0.0 {
		set => max(0.0, $value);
	}

	public bool $visible {
		get => $this->radius > 0;
	}
}

Μέθοδοι επέκτασης

Το Nette\Object έφερε στην PHP μια άλλη ενδιαφέρουσα έννοια εμπνευσμένη από τις σύγχρονες γλώσσες προγραμματισμού – τις μεθόδους επέκτασης. Αυτή η λειτουργία, δανεισμένη από τη C#, επέτρεψε στους προγραμματιστές να επεκτείνουν κομψά τις υπάρχουσες κλάσεις με νέες μεθόδους χωρίς να χρειάζεται να τις τροποποιήσουν ή να κληρονομήσουν από αυτές. Για παράδειγμα, θα μπορούσατε να προσθέσετε στη φόρμα μια μέθοδο addDateTime(), η οποία προσθέτει ένα προσαρμοσμένο DateTimePicker:

Form::extensionMethod(
	'addDateTime',
	fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);

$form = new Form;
$form->addDateTime('date');

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

Λήψη ονόματος κλάσης

Για τη λήψη του ονόματος της κλάσης, το SmartObject προσέφερε μια απλή μέθοδο:

$class = $obj->getClass(); // με το Nette\Object
$class = $obj::class;      // από την PHP 8.0

Πρόσβαση σε reflection και annotations

Το Nette\Object προσέφερε πρόσβαση σε reflection και annotations χρησιμοποιώντας τις μεθόδους getReflection() και getAnnotation(). Αυτή η προσέγγιση απλοποίησε σημαντικά την εργασία με τις μετα-πληροφορίες των κλάσεων:

/**
 * @author John Doe
 */
class Foo extends Nette\Object
{
}

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // επιστρέφει 'John Doe'

Από την PHP 8.0, είναι δυνατή η πρόσβαση στις μετα-πληροφορίες με τη μορφή attributes, τα οποία προσφέρουν ακόμη περισσότερες δυνατότητες και καλύτερο έλεγχο τύπων:

#[Author('John Doe')]
class Foo
{
}

$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];

Getters μεθόδων

Το Nette\Object προσέφερε έναν κομψό τρόπο για να περνάτε μεθόδους σαν να ήταν μεταβλητές:

class Foo extends Nette\Object
{
	public function adder($a, $b)
	{
		return $a + $b;
	}
}

$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5

Από την PHP 8.1, είναι δυνατή η χρήση της λεγόμενης first-class callable syntax, η οποία προχωρά αυτή την έννοια ακόμη παραπέρα:

$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5

Συμβάντα

Το SmartObject προσφέρει μια απλοποιημένη σύνταξη για την εργασία με συμβάντα. Τα συμβάντα επιτρέπουν στα αντικείμενα να ενημερώνουν άλλα μέρη της εφαρμογής για αλλαγές στην κατάστασή τους:

class Circle extends Nette\Object
{
	public array $onChange = [];

	public function setRadius(float $radius): void
	{
		$this->onChange($this, $radius);
		$this->radius = $radius;
	}
}

Ο κώδικας $this->onChange($this, $radius) είναι ισοδύναμος με τον ακόλουθο βρόχο:

foreach ($this->onChange as $callback) {
	$callback($this, $radius);
}

Για λόγους σαφήνειας, συνιστούμε να αποφεύγετε τη μαγική μέθοδο $this->onChange(). Μια πρακτική αντικατάσταση είναι, για παράδειγμα, η συνάρτηση Nette\Utils\Arrays::invoke:

Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);
έκδοση: 4.0