SmartObject

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

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

composer require nette/utils

Ιδιότητες, Getters και Setters

Στις σύγχρονες αντικειμενοστραφείς γλώσσες (π.χ. C#, Python, Ruby, JavaScript), ο όρος ιδιότητα αναφέρεται σε ειδικά μέλη των κλάσεων που μοιάζουν με μεταβλητές αλλά στην πραγματικότητα αντιπροσωπεύονται από μεθόδους. Όταν η τιμή αυτής της “μεταβλητής” εκχωρείται ή διαβάζεται, καλείται η αντίστοιχη μέθοδος (που ονομάζεται getter ή setter). Αυτό είναι κάτι πολύ βολικό, μας δίνει πλήρη έλεγχο της πρόσβασης στις μεταβλητές. Μπορούμε να επικυρώσουμε την είσοδο ή να δημιουργήσουμε αποτελέσματα μόνο όταν διαβάζεται η ιδιότητα.

Οι ιδιότητες της PHP δεν υποστηρίζονται, αλλά το trait Nette\SmartObject μπορεί να τις μιμηθεί. Πώς να το χρησιμοποιήσετε;

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

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

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

	private float $radius = 0.0; // όχι δημόσια

	// 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()

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

Μια ματιά στην ιστορία

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

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

Καθώς η ανάπτυξη της PHP συνεχιζόταν, το μοντέλο αντικειμένων και οι δυνατότητες της γλώσσας βελτιώνονταν. Διάφορες λειτουργίες της κλάσης SmartObject έγιναν περιττές. Από την έκδοση της PHP 8.2, παραμένει μόνο ένα χαρακτηριστικό που δεν υποστηρίζεται άμεσα στην PHP: η δυνατότητα χρήσης των λεγόμενων ιδιοτήτων.

Ποια χαρακτηριστικά προσέφερε η Nette\Object και, κατ' επέκταση, η Nette\SmartObject; Ακολουθεί μια επισκόπηση. (Στα παραδείγματα χρησιμοποιείται η κλάση Nette\Object, αλλά τα περισσότερα χαρακτηριστικά ισχύουν και για την ιδιότητα Nette\SmartObject ).

Ασυνεπή σφάλματα

Η PHP είχε ασυνεπή συμπεριφορά κατά την πρόσβαση σε μη δηλωμένα μέλη. Η κατάσταση κατά τη στιγμή του Nette\Object ήταν η εξής:

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

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

echo $obj->undeclared;   // throw Nette\MemberAccessException
$obj->undeclared = 1;    // throw Nette\MemberAccessException
$obj->unknownMethod();   // throw Nette\MemberAccessException

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

Εννοείτε?

Εάν εμφανιζόταν ένα σφάλμα Nette\MemberAccessException, ίσως λόγω τυπογραφικού λάθους κατά την προσπέλαση μιας μεταβλητής αντικειμένου ή την κλήση μιας μεθόδου, το Nette\Object προσπαθούσε να δώσει μια υπόδειξη στο μήνυμα σφάλματος για το πώς να διορθωθεί το σφάλμα, με τη μορφή της εικονικής προσθήκης “εννοούσες;”.

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

$foo = Foo::form($var);
// throw Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"

Αν και η σημερινή PHP δεν διαθέτει τη δυνατότητα “εννοούσατε;”, αυτή η φράση μπορεί να προστεθεί στα σφάλματα από την Tracy. Μπορεί ακόμη και να διορθώσει αυτόματα τέτοια σφάλματα.

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

Εμπνευσμένο από τις μεθόδους επέκτασης της C#. Έδιναν τη δυνατότητα προσθήκης νέων μεθόδων σε υπάρχουσες κλάσεις. Για παράδειγμα, θα μπορούσατε να προσθέσετε τη μέθοδο addDateTime() σε μια φόρμα για να προσθέσετε το δικό σας DateTimePicker.

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

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

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

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

$class = $obj->getClass(); // χρησιμοποιώντας το Nette\Object
$class = $obj::class;      // από την PHP 8.0

Πρόσβαση στον Αναστοχασμό και τις Σημειώσεις

Nette\Object προσφέρεται πρόσβαση στον προβληματισμό και τον σχολιασμό με τη χρήση των μεθόδων getReflection() και getAnnotation():

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

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // returns 'John Doe'

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

#[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, μπορείτε να χρησιμοποιήσετε τη λεγόμενη σύνταξη κλήσης πρώτης κατηγορίας:

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

Γεγονότα

Nette\Object προσφέρεται συντακτική ζάχαρη για την ενεργοποίηση του συμβάντος:

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