SmartObject

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

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

composer require nette/utils

Το SmartObject δημιουργήθηκε το 2007 ως μια επαναστατική λύση στις ελλείψεις του τότε μοντέλου αντικειμένων της PHP. Σε μια εποχή που η PHP αντιμετώπιζε πολλά προβλήματα με τον αντικειμενοστρεφή σχεδιασμό, έφερε σημαντικές βελτιώσεις και απλοποίησε την εργασία των προγραμματιστών. Έγινε θρυλικό μέρος του framework Nette. Πρόσφερε λειτουργικότητα που η 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, η πρόσβαση σε μη δηλωμένα μέλη θεωρείται σφάλμα.

Βοήθεια “Did you mean?”

Το 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 ήταν οι ιδιότητες με ελεγχόμενη πρόσβαση (properties). Αυτή η έννοια, κοινή σε γλώσσες όπως η C# ή η Python, επέτρεψε στους προγραμματιστές να ελέγχουν κομψά την πρόσβαση στα δεδομένα αντικειμένων και να διασφαλίζουν τη συνέπειά τους. Οι ιδιότητες είναι ένα ισχυρό εργαλείο αντικειμενοστρεφούς προγραμματισμού. Λειτουργούν ως μεταβλητές αλλά στην πραγματικότητα αντιπροσωπεύονται από μεθόδους (getters και setters). Αυτό επιτρέπει την επικύρωση εισόδων ή τη δημιουργία τιμών τη στιγμή της ανάγνωσης.

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

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

Ας δούμε ένα πρακτικό παράδειγμα χρησιμοποιώντας την κλάση 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;
	}
}

Extension Methods

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

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

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

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

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

Το 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];

Method 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

Συμβάντα (Events)

Το 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);