composer require okapi/aop
AOP : Programmation orientée vers l'aspect - Un paradigme de programmation qui vise à augmenter la modularité en permettant la séparation des préoccupations croisées.
Aspect : une classe qui implémente la logique que vous souhaitez appliquer à vos classes cibles. Les aspects doivent être annotés avec l'attribut #[Aspect]
.
Conseil : la logique que vous souhaitez appliquer à vos classes cibles. Les méthodes de conseil doivent être annotées avec les attributs #[Before]
, #[Around]
ou #[After]
.
Point de jointure : un point dans l'exécution de vos classes cibles où vous pouvez appliquer vos conseils. Les points de jointure sont définis par les attributs #[Before]
, #[Around]
ou #[After]
.
PointCut : un ensemble de points de jointure où vous pouvez appliquer vos conseils. Les coupes ponctuelles sont définies par l'attribut #[Pointcut]
.
Tissage : le processus d'application de vos conseils à vos classes cibles.
Aspects implicites : Les aspects sont appliqués sans aucune modification des classes cibles. L'aspect lui-même spécifie les classes ou les méthodes auxquelles il s'applique.
Aspects explicites au niveau de la classe : Les aspects sont appliqués en modifiant les classes cibles, généralement en ajoutant l'aspect en tant qu'attribut à la classe cible.
Aspects explicites au niveau de la méthode : les aspects sont appliqués en modifiant les classes cibles, généralement en ajoutant l'aspect en tant qu'attribut à la méthode cible.
<?php
use Okapi Aop AopKernel ;
// Extend from the "AopKernel" class
class MyKernel extends AopKernel
{
// Define a list of aspects
protected array $ aspects = [
DiscountAspect ::class,
PaymentProcessorAspect ::class,
];
}
// Discount Aspect
<?php
use Okapi Aop Attributes Aspect ;
use Okapi Aop Attributes After ;
use Okapi Aop Invocation AfterMethodInvocation ;
// Aspects must be annotated with the "Aspect" attribute
#[ Aspect ]
class DiscountAspect
{
// Annotate the methods that you want to intercept with
// "Before", "Around" or "After" attributes
#[ After (
// Use named arguments
// You can also use Wildcards (see Okapi/Wildcards package)
class: Product ::class . ' | ' . Order ::class,
method: ' get(Price|Total) ' ,
// When using wildcards you can also use some of these options:
onlyPublicMethods: false , // Intercepts only public methods and ignores protected and private methods (default: false)
interceptTraitMethods: true , // Also intercepts methods from traits (default: true)
)]
public function applyDiscount ( AfterMethodInvocation $ invocation ): void
{
// Get the subject of the invocation
// The subject is the object class that contains the method
// that is being intercepted
$ subject = $ invocation -> getSubject ();
$ productDiscount = 0.1 ;
$ orderDiscount = 0.2 ;
if ( $ subject instanceof Product ) {
// Get the result of the original method
$ oldPrice = $ invocation -> proceed ();
$ newPrice = $ oldPrice - ( $ oldPrice * $ productDiscount );
// Set the new result
$ invocation -> setResult ( $ newPrice );
}
if ( $ subject instanceof Order ) {
$ oldTotal = $ invocation -> proceed ();
$ newTotal = $ oldTotal - ( $ oldTotal * $ orderDiscount );
$ invocation -> setResult ( $ newTotal );
}
}
}
// PaymentProcessor Aspect
<?php
use InvalidArgumentException ;
use Okapi Aop Attributes After ;
use Okapi Aop Attributes Around ;
use Okapi Aop Attributes Aspect ;
use Okapi Aop Attributes Before ;
use Okapi Aop Invocation AroundMethodInvocation ;
use Okapi Aop Invocation AfterMethodInvocation ;
use Okapi Aop Invocation BeforeMethodInvocation ;
#[ Aspect ]
class PaymentProcessorAspect
{
#[ Before (
class: PaymentProcessor ::class,
method: ' processPayment ' ,
)]
public function checkPaymentAmount ( BeforeMethodInvocation $ invocation ): void
{
$ payment = $ invocation -> getArgument ( ' amount ' );
if ( $ payment < 0 ) {
throw new InvalidArgumentException ( ' Invalid payment amount ' );
}
}
#[ Around (
class: PaymentProcessor ::class,
method: ' processPayment ' ,
)]
public function logPayment ( AroundMethodInvocation $ invocation ): void
{
$ startTime = microtime ( true );
// Proceed with the original method
$ invocation -> proceed ();
$ endTime = microtime ( true );
$ elapsedTime = $ endTime - $ startTime ;
$ amount = $ invocation -> getArgument ( ' amount ' );
$ logMessage = sprintf (
' Payment processed for amount $%.2f in %.2f seconds ' ,
$ amount ,
$ elapsedTime ,
);
// Singleton instance of a logger
$ logger = Logger :: getInstance ();
$ logger -> log ( $ logMessage );
}
#[ After (
class: PaymentProcessor ::class,
method: ' processPayment ' ,
)]
public function sendEmailNotification ( AfterMethodInvocation $ invocation ): void
{
// Proceed with the original method
$ result = $ invocation -> proceed ();
$ amount = $ invocation -> getArgument ( ' amount ' );
$ message = sprintf (
' Payment processed for amount $%.2f ' ,
$ amount ,
);
if ( $ result === true ) {
$ message .= ' - Payment successful ' ;
} else {
$ message .= ' - Payment failed ' ;
}
// Singleton instance of an email queue
$ mailQueue = MailQueue :: getInstance ();
$ mailQueue -> addMail ( $ message );
}
}
// Product
<?php
class Product
{
private float $ price ;
public function getPrice (): float
{
return $ this -> price ;
}
}
// Order
<?php
class Order
{
private float $ total = 500.00 ;
public function getTotal (): float
{
return $ this -> total ;
}
}
// PaymentProcessor
<?php
class PaymentProcessor
{
public function processPayment ( float $ amount ): bool
{
// Process payment
return true ;
}
}
// Initialize the kernel early in the application lifecycle
// Preferably after the autoloader is registered
<?php
use MyKernel ;
require_once __DIR__ . ' /vendor/autoload.php ' ;
// Initialize the AOP Kernel
$ kernel = MyKernel :: init ();
<?php
// Just use your classes as usual
$ product = new Product ();
// Before AOP: 100.00
// After AOP: 90.00
$ productPrice = $ product -> getPrice ();
$ order = new Order ();
// Before AOP: 500.00
// After AOP: 400.00
$ orderTotal = $ order -> getTotal ();
$ paymentProcessor = new PaymentProcessor ();
// Invalid payment amount
$ amount = - 50.00 ;
// Before AOP: true
// After AOP: InvalidArgumentException
$ paymentProcessor -> processPayment ( $ amount );
// Valid payment amount
$ amount = 100.00 ;
// Value: true
$ paymentProcessor -> processPayment ( $ amount );
$ logger = Logger :: getInstance ();
$ logs = $ logger -> getLogs ();
// Value: Payment processed for amount $100.00 in 0.00 seconds
$ firstLog = $ logs [ 0 ];
$ mailQueue = MailQueue :: getInstance ();
$ mails = $ mailQueue -> getMails ();
// Value: Payment processed for amount $100.00 - Payment successful
$ firstMail = $ mails [ 0 ];
L'ajout de l'aspect personnalisé au noyau n'est pas requis pour les aspects explicites au niveau de la classe car ils sont enregistrés automatiquement au moment de l'exécution.
// Logging Aspect
<?php
use Attribute ;
use Okapi Aop Attributes Aspect ;
use Okapi Aop Attributes Before ;
use Okapi Aop Invocation BeforeMethodInvocation ;
// Class-Level Explicit Aspects must be annotated with the "Aspect" attribute
// and the "Attribute" attribute
#[ Attribute ]
#[ Aspect ]
class LoggingAspect
{
// The "class" argument is not required
// The "method" argument is optional
// Without the argument, the aspect will be applied to all methods
// With the argument, the aspect will be applied to the specified method
#[ Before ]
public function logAllMethods ( BeforeMethodInvocation $ invocation ): void
{
$ methodName = $ invocation -> getMethodName ();
$ logMessage = sprintf (
" Method '%s' executed. " ,
$ methodName ,
);
$ logger = Logger :: getInstance ();
$ logger -> log ( $ logMessage );
}
#[ Before (
method: ' updateInventory ' ,
)]
public function logUpdateInventory ( BeforeMethodInvocation $ invocation ): void
{
$ methodName = $ invocation -> getMethodName ();
$ logMessage = sprintf (
" Method '%s' executed. " ,
$ methodName ,
);
$ logger = Logger :: getInstance ();
$ logger -> log ( $ logMessage );
}
}
// Inventory Tracker
<?php
// Custom Class-Level Explicit Aspect added to the class
#[ LoggingAspect ]
class InventoryTracker
{
private array $ inventory = [];
public function updateInventory ( int $ productId , int $ quantity ): void
{
$ this -> inventory [ $ productId ] = $ quantity ;
}
public function checkInventory ( int $ productId ): int
{
return $ this -> inventory [ $ productId ] ?? 0 ;
}
}
// Initialize the kernel early in the application lifecycle
// Preferably after the autoloader is registered
// The kernel must still be initialized, even if it has no Aspects
<?php
use MyKernel ;
require_once __DIR__ . ' /vendor/autoload.php ' ;
// Initialize the AOP Kernel
$ kernel = MyKernel :: init ();
<?php
// Just use your classes as usual
$ inventoryTracker = new InventoryTracker ();
$ inventoryTracker -> updateInventory ( 1 , 100 );
$ inventoryTracker -> updateInventory ( 2 , 200 );
$ countProduct1 = $ inventoryTracker -> checkInventory ( 1 );
$ countProduct2 = $ inventoryTracker -> checkInventory ( 2 );
$ logger = Logger :: getInstance ();
// Value:
// Method 'updateInventory' executed. (4 times)
// Method 'checkInventory' executed. (2 times)
$ logs = $ logger -> getLogs ();
L'ajout de l'aspect personnalisé au noyau n'est pas requis pour les aspects explicites au niveau de la méthode car ils sont enregistrés automatiquement au moment de l'exécution.
// Performance Aspect
<?php
use Attribute ;
use Okapi Aop Attributes Around ;
use Okapi Aop Invocation AroundMethodInvocation ;
use Okapi Aop Attributes Aspect ;
// Method-Level Explicit Aspects must be annotated with the "Aspect" attribute
// and the "Attribute" attribute
#[ Attribute ]
#[ Aspect ]
class PerformanceAspect
{
// The "class" argument is not required
// The "method" argument is optional
// Without the argument, the aspect will be applied to all methods
// With the argument, the aspect will be applied to the specified method
#[ Around ]
public function measure ( AroundMethodInvocation $ invocation ): void
{
$ start = microtime ( true );
$ invocation -> proceed ();
$ end = microtime ( true );
$ executionTime = $ end - $ start ;
$ class = $ invocation -> getClassName ();
$ method = $ invocation -> getMethodName ();
$ logMessage = sprintf (
" Method %s::%s executed in %.2f seconds. " ,
$ class ,
$ method ,
$ executionTime ,
);
$ logger = Logger :: getInstance ();
$ logger -> log ( $ logMessage );
}
}
// Customer Service
<?php
class CustomerService
{
#[ PerformanceAspect ]
public function createCustomer (): void
{
// Logic to create a customer
}
}
// Initialize the kernel early in the application lifecycle
// Preferably after the autoloader is registered
// The kernel must still be initialized, even if it has no Aspects
<?php
use MyKernel ;
require_once __DIR__ . ' /vendor/autoload.php ' ;
// Initialize the AOP Kernel
$ kernel = MyKernel :: init ();
<?php
// Just use your classes as usual
$ customerService = new CustomerService ();
$ customerService -> createCustomer ();
$ logger = Logger :: getInstance ();
$ logs = $ logger -> getLogs ();
// Value: Method CustomerService::createCustomer executed in 0.01 seconds.
$ firstLog = $ logs [ 0 ];
Types de conseils: "avant", "autour" et "après"
Intercepter les méthodes «privées» et «protégées» (affichera des erreurs dans les IDE)
Accédez aux propriétés et méthodes «privées» et «protégées» du sujet (affichera des erreurs dans les IDE)
Intercepter les méthodes et classes «finales»
Utilisez des transformateurs du package "OKAPI / Code-Transformateur" dans votre noyau pour modifier et transformer le code source d'une classe PHP chargée (voir le package "OKAPI / Code-Transformateur" pour plus d'informations)
Ce package étend le package "okapi / code-transformateur" avec injection de dépendance et fonctionnalités AOP
L' AopKernel
enregistre plusieurs services
Le service TransformerManager
stocke la liste des aspects et leur configuration
Le service CacheStateManager
gère l'état de cache
Le service StreamFilter
enregistre un filtre de flux PHP qui permet de modifier le code source avant qu'il ne soit chargé par PHP
Le service AutoloadInterceptor
surcharge le compositeur Autoloader, qui gère le chargement des classes
Le service AutoloadInterceptor
intercepte le chargement d'une classe
L' AspectMatcher
correspond à la classe et aux noms de méthode avec la liste des aspects et leur configuration
Si les noms de classe et de méthode correspondent à un aspect, interrogez l'état de cache pour voir si le code source est déjà mis en cache
Vérifiez si le cache est valide:
Si le cache est valide, chargez la classe procassée du cache
Sinon, renvoyez un chemin de filtre de flux vers le service AutoloadInterceptor
Le StreamFilter
modifie le code source en appliquant les aspects
composer run-script test
composer run-script test-coverage
<?php
use Okapi Aop Attributes After ;
use Okapi Aop Attributes Aspect ;
use Okapi Aop Invocation AfterMethodInvocation ;
#[ Aspect ]
class EverythingAspect
{
#[ After (
class: ' * ' ,
method: ' * ' ,
)]
public function everything ( AfterMethodInvocation $ invocation ): void
{
echo $ invocation -> getClassName () . "n" ;
echo $ invocation -> getMethodName () . "n" ;
}
}
Donnez un si ce projet vous a aidé!
Copyright © 2023 Valentin Wotschel.
Ce projet est sous licence MIT.