2017-02-19
Kódbiztonság a Magento 2-ben: Interceptor
A Magento számos kódszintű újdonságai közül az egyik legkiemelkedőbb (a modulok fejlesztése során) az interceptorok megjelenése a 2.0 verziótól kezdődően. Az Interceptor a Magento 1-ben is használt Observerhez hasonló funkcionalitást valósít meg, azonban mindezt jóval biztonságosabban teszi. A Magento 2-ben a szervizek megjelenésével számos eddig ismert observer fog eltűnni és nem feltétlenül kerülnek újak a helyükbe. Mindez nem vesz el semmit a Magentoból, inkább hozzá ad valamit, ami jobb.
Egy korábbi cikkben már nagy vonalakban bemutatásra kerültek a Magento 2-ben használt objektum orientált szoftver design patternek. Ez a cikk az Interceptor patternt mutatja be részletesebben.
Az Observer minta és veszélye
Magento 1-ben lehetséges volt egy termék objektumot a save metódus meghívásával elmenteni, a save metódus pedig megtriggerelte a termék mentése eseményt, amelyre bármilyen modul feliratkozhatott és manipulálhatta az elmentésre szánt adatokat. Ez jelentősen megkönnyíti a modulok közötti kommunikációt, illetve remekül bővíthetővé teszi a Magentot, viszont számos biztonsági rést nyit a mentési folyamatban azáltal, hogy különböző modulok beleszólhatnak a folyamatba, akár hibát okozhatnak a mentés során.
A Magento 2-ben továbbra is jelen van az Observer mint core szinten alkalmazott pattern. Azonban a szervizek megjelenése miatt, már nem lehetséges a termék objektumon mentést végezni, a termék objektumot egy szerviznek kell átadni, ahol megtörténik a termék mentése, perzisztálása. Ennek következtében már termék mentés esemény sincs, illetve deprecated jelöléssel még létezik a Magento 2-ben, de ezek a funkciók, események folyamatosan kerülnek ki a core kódjaiból.
De miért nem kerülnek új observerek a szervizekbe és egyéb modellekbe? A válasz: a biztonság miatt. Az observerhez tartozó események a core modellek public, protected és private metódusaiban is szerepelnek, vagyis akár private metódusokhoz is globális hozzáférést engedélyeznek. Ez komoly rés az osztályok zártságában, ha pedig egy osztály nem is zárt, akkor eleve a ráépülő kód sem lehet objektum orientált. A cél viszont egyértelműen a biztonságos objektum orientált kód megvalósítása.
A megoldás: Interceptor tervezési minta
Tehát azáltal, hogy nem (vagy csak elvétve) kerülnek be új observerek a Magento 2-be, maga szoftver biztonságosabbá válik, azonban ezzel együtt kevésbé bővíthetővé. Ennek az akadálynak a leküzdésére hozták be az Interceptor objektum orientált tervezési mintát és alkalmazták a Magento 2-ben core szinten, akár csak a Factory, Singleton vagy Proxy patterneket.
Az Interceptor tervezési minta lehetővé teszi, hogy manipuláljuk az osztályok publikus metódusainak visszatérési értékét, anélkül, hogy az adott metódus osztályát extendálnánk vagy magentos módon felülírnánk. A Magento 2-ben mindez core szinten valósul meg. Vagyis minden osztály köré egy-egy Interceptor épül anélkül, hogy nekünk ehhez bármit is tenni kellene.
Belülről, a Magento frameworkből nézve
Hogyan is működik mindez a gyakorlatban? Nézzünk meg egy osztályt: Magento/Framework/Model/ResourceModel/Db/AbstractDb. Ennek az AbstractDb osztálynak van egy save metódusa, amely az alábbi képen is látható. Érdemes megfigyelni, hogy a metódusban nincsen event dispatch, vagy is nem tud observer csatlakozni a függvényre.
A fenti save metódus szervizből hívható, feladata egy entitást elmenteni az adatbázisba, pl. egy terméket. Azonban a termék mentés hatására a termékkel kapcsolatos cache-nek törlődnie kell. Ha nincs esemény a save-ben, akkor nincs observer, ami feliratkozhat, így a cache-t a megszokott módon nem lehet törölni. Mivel a save metódus publikus, ezért Interceptor is tud köré épülni. Azt, hogy melyik osztály köré melyik Interceptor épüljön a Depency Injection konfigurációs XML-ben adhatjuk meg:
A fenti képen látható, hogy az adott modul di.xml fájljában egy type XML tag meghatározza az eredeti AbstractDb osztályt és a type-on belül egy plugin XML tag szerepel, amely a FlushCacheByTags plugin osztályunkra hivatkozik.
A Magentonak az XML-en keresztül mondjuk meg, hogy a FlushCacheByTags plugin osztály az AbstractDb osztályt hookolja, és ennek hatására a Magento legyártja az Interceptor osztályt. Developer módban mindez on-the-fly történik, production módban pedig a dependency injection compile parancsát futtatva generálja le magát az Interceptor PHP osztályait és a .ser kiterjesztésű fájlokat, amelyek a dependenciákat tartalmazzák.
Ha megnézzük a FlushCacheByTags osztályt (ez a plugin osztály, amelyből, majd az Interceptor készül) akkor láthatjuk, hogy ez az osztály nem örököl meg másik osztályt vagyis nem ír felül semmit. Itt található egy aroundSave metódus. Ez akár lehetne még afterSave, illetve beforeSave is. Az afterSave metódus az után hívódna meg miután a save metódus lefutott, a beforeSave pedig előtte, míg az aroundSave teljesen körül öleli a metódust. Vagyis ha egy olyan objektumon kerül meghívásra a save metódus, amely az AbstractDb osztályt örökli meg, akkor elsőként az aroundSave fog meghívódni, majd azon belül az eredeti save metódus.
Az aroundSave metódus három paramétert kap meg.
- Az első a $subject, amely maga az objektum, amelyet körül ölel majd az Interceptor. Itt az AbstractResource van meghatározva típusként, hiszen az AbstractDb ennek az osztálynak a leszármazottja.
- A második paraméter a $proceed, egy Closure, amely a PHP-ban az anonymus függvény típusa. Ez a paraméter az eredeti save metódus.
- A harmadik paraméter pedig az $object, az eredeti save metódus egyetlen bemenő paramétere - az az objektum amely elmentésre kerül.
Az aroundSave törzsében látható, hogy a $proceed paraméterként megkapja az $object változót, vagyis itt valósul meg a mentés, ami eredetileg is megtörténne. A $proceed visszaadja, amit a save adna vissza eredetileg. Ez az érték a $result változóba kerül és a függvény végén az aroundSave ezt a $result változót adja vissza. Viszont a $proceed meghívása és a $result visszaadása között megtörténik az, amiért használjuk az Interceptort: Egy elágazásban vizsgáljuk, hogy az elmentett objektumunknak van e entitás azonosítója, ha van, akkor töröljük az cache-t az azonosító alapján.
Nagyobb változás, kevesebb gond
Ha újra átnézzük, hogy mi is történik ezekben a kódokban, akkor láthatjuk, hogy az eredeti AbstractDb osztály egyáltalán nincs felkészítve semmilyen kiegészítésre, hookolára, mégis manipulálható/kiegészíthető a működése a Magento Interceptorának felhasználásával. Vagyis a végeredmény nem csak biztonságosabb kód lett az Observer mintával szemben, hanem a jövőbeli kiegészítések is egyszerűbbé válnak attól, mint amilyenek voltak a régi Magento 1 Observer központú architekturájában.
Az Interceptor alapjaiban változtatja meg azt, hogy a modulok, hogyan kommunikálnak egymással, azonban a fenti módszerrel előre való gondolkodás nélkül egészíthetjük ki akár saját modulunk vagy core modulok osztályait anélkül, hogy ez a kód minőségének vagy biztonságának a rovására menne. Az Interceptor bevezetése jó példa arra, hogy minőséget megkövetelő nagyobb horderejű változások végsősoron nem megnehezítik, hanem megkönnyítik a fejlesztők munkáját.