Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Magento 2 - An Intro to a Modern PHP-Based System - Northeast PHP 2015

Magento 2 - An Intro to a Modern PHP-Based System - Northeast PHP 2015

Over 200,000 companies use the Magento 1 platform to power their eCommerce needs. So when they set out to build a major new version, the Magento team had significant pressure to deliver a modern, well-designed PHP-based system. Pulling in some of the best of the PHP world through tools like Composer, phpunit and more, I believe they met that goal. In this talk, we’ll take a look at the design and architecture of Magento 2, including it’s use of dependency injections, interceptors and service contracts to provide numerous ways for developers to extend and customize the system.

Presented at Northeast PHP 2015

Joshua Warren

August 23, 2015
Tweet

More Decks by Joshua Warren

Other Decks in Technology

Transcript

  1. PRESENTED BY
    JOSHUA WARREN
    PRESENTED AT
    NORTHEAST PHP 2015
    Magento 2
    AN INTRODUCTION
    TO A MODERN PHP-
    BASED SYSTEM

    View Slide

  2. MY
    EXPERIENCE

    View Slide

  3. JoshuaWarren.com
    My Experience
    PHP Developer Since 1999
    Founded Creatuity in 2008
    Focused on the Magento platform
    Magento 2 contributor
    #NEPHP

    View Slide

  4. JoshuaWarren.com
    early adopter of both Magento 1
    and Magento 2
    #NEPHP

    View Slide

  5. JoshuaWarren.com
    Frequent Magento presenter
    #NEPHP

    View Slide

  6. JoshuaWarren.com
    Active member of the
    #RealMagento community
    #NEPHP

    View Slide

  7. JoshuaWarren.com
    Involved in feedback and design
    discussions throughout the
    Magento 2 Developer Beta
    #NEPHP

    View Slide

  8. JoshuaWarren.com
    Wrote Writing the book on
    Magento 2
    #NEPHP

    View Slide

  9. A BRIEF
    HISTORY OF
    MAGENTO
    Photo courtesy of @YoavKutner

    View Slide

  10. JoshuaWarren.com
    Magento 1 development began in
    2007 by Varien, a PHP development
    agency.
    #NEPHP

    View Slide

  11. JoshuaWarren.com
    In 2007, osCommerce was state of
    the art.
    #NEPHP

    View Slide

  12. JoshuaWarren.com
    Cloud-based eCommerce systems
    didn’t exist.
    #NEPHP

    View Slide

  13. JoshuaWarren.com
    PHP 5.2 was cutting-edge.
    #NEPHP

    View Slide

  14. JoshuaWarren.com
    Composer didn’t exist, and Zend
    Framework 1 was still in early beta.
    #NEPHP

    View Slide

  15. JoshuaWarren.com
    Magento 1 was built to resolve the
    pain points of osCommerce.
    #NEPHP

    View Slide

  16. JoshuaWarren.com
    Designed to be more flexible and to
    provide standardized ways to
    customize the platform.
    #NEPHP

    View Slide

  17. JoshuaWarren.com
    By 2010, Magento had been
    downloaded 1.5 million times.
    #NEPHP

    View Slide

  18. JoshuaWarren.com
    Attracted by Magento’s free, open-
    source approach, hundreds of
    thousands of sites were launched
    using Magento 1.
    #NEPHP

    View Slide

  19. JoshuaWarren.com
    There’s just one problem…
    #NEPHP

    View Slide

  20. JoshuaWarren.com
    Ecommerce development is hard.
    #NEPHP

    View Slide

  21. View Slide

  22. JoshuaWarren.com
    Lightly documented ecommerce
    development is even harder.
    #NEPHP

    View Slide

  23. View Slide

  24. JoshuaWarren.com
    Varien, now known as Magento Inc,
    is acquired by eBay in 2011.
    #NEPHP

    View Slide

  25. JoshuaWarren.com
    Work begins on Magento 2 almost
    immediately.
    #NEPHP

    View Slide

  26. JoshuaWarren.com
    Now the most widely-used
    eCommerce platform, powering over
    250,000 sites, expectations are high
    for the Magento 2 team.
    #NEPHP

    View Slide

  27. JoshuaWarren.com
    Fewer than 30 commits are made to
    Magento 2 in 2012.
    #NEPHP

    View Slide

  28. JoshuaWarren.com
    Internal priorities continue to shift,
    and finally at the end of 2014,
    Magento 2 development is made
    public on Github.
    #NEPHP

    View Slide

  29. JoshuaWarren.com
    Magento commits to a release
    schedule for Magento 2, and
    announces the acceptance of pull
    requests.
    #NEPHP

    View Slide

  30. JoshuaWarren.com
    devdocs.magento.com launches with
    significant documentation of
    Magento 2.
    #NEPHP

    View Slide

  31. JoshuaWarren.com
    Developer Beta is released in
    December 2014; Merchant Beta in
    July 2015
    #NEPHP

    View Slide

  32. JoshuaWarren.com
    Production-ready release (‘general
    availability’) scheduled for Q4 2015
    #NEPHP

    View Slide

  33. MAGENTO 2
    github.com/magento/magento2

    View Slide

  34. JoshuaWarren.com
    Feature parity with Magento 1 +
    UI/UX Improvements + focus on
    resolving technical debt
    #NEPHP

    View Slide

  35. JoshuaWarren.com
    Technologies
    #NEPHP

    View Slide

  36. JoshuaWarren.com #NEPHP
    Composer
    composer create-project magento/product-community-edition --stability="beta"

    View Slide

  37. JoshuaWarren.com
    Each Magento 2 module is a
    separate Composer package
    #NEPHP

    View Slide

  38. JoshuaWarren.com
    PSR-0 thru PSR-4
    #NEPHP

    View Slide

  39. JoshuaWarren.com
    Testing built in from the start.
    phpunit, selenium, JMeter, Jasmine
    #NEPHP

    View Slide

  40. JoshuaWarren.com
    magento2/dev/tests/
    #NEPHP

    View Slide

  41. JoshuaWarren.com
    HTML5, CSS3, LESS CSS
    Preprocessor, JQuery, RequireJS
    #NEPHP

    View Slide

  42. JoshuaWarren.com
    Components from Zend
    Framework 1, Zend Framework 2,
    Symfony
    #NEPHP

    View Slide

  43. JoshuaWarren.com
    Technical Architecture
    #NEPHP

    View Slide

  44. JoshuaWarren.com
    Presentation Layer, Service Layer,
    Domain Layer, Persistence Layer
    #NEPHP

    View Slide

  45. JoshuaWarren.com #NEPHP

    View Slide

  46. JoshuaWarren.com
    Presentation Layer - views,
    literally and figuratively
    #NEPHP

    View Slide

  47. JoshuaWarren.com
    Service Layer - an intermediary
    between the presentation and
    model layers
    #NEPHP

    View Slide

  48. JoshuaWarren.com
    Service layer provides a stable,
    backwards-compatible interface
    and forms the foundation for
    dependency injection.
    #NEPHP

    View Slide

  49. JoshuaWarren.com
    Domain layer - business logic,
    including models. Contains the
    implementation of service
    contracts.
    #NEPHP

    View Slide

  50. JoshuaWarren.com
    Persistence Layer - resource
    models that perform CRUD
    operations on database tables.
    #NEPHP

    View Slide

  51. JoshuaWarren.com
    Some models use a single table,
    others continue to use the
    Entity-Attribute-Value design
    pattern used in Magento 1.
    #NEPHP

    View Slide

  52. JoshuaWarren.com
    Design Patterns
    #NEPHP

    View Slide

  53. JoshuaWarren.com
    Loose Coupling
    #NEPHP

    View Slide

  54. JoshuaWarren.com
    Dependency Injection
    #NEPHP

    View Slide

  55. JoshuaWarren.com
    Service Contracts
    #NEPHP

    View Slide

  56. JoshuaWarren.com
    Interceptors
    #NEPHP

    View Slide

  57. JoshuaWarren.com
    Semantic Versioning
    #NEPHP

    View Slide

  58. DEPENDENCY
    INJECTION
    Sorry - no cool photo here, because I don’t like needles…

    View Slide

  59. JoshuaWarren.com
    DI is exactly what it sounds like -
    injecting dependencies into the
    objects that need them.
    #NEPHP

    View Slide

  60. JoshuaWarren.com
    DI is designed to reduce
    dependencies and promote loose
    coupling
    #NEPHP

    View Slide

  61. JoshuaWarren.com
    DI makes unit testing much easier
    #NEPHP

    View Slide

  62. JoshuaWarren.com
    Magento 2 uses the Constructor
    Injection pattern of DI
    #NEPHP

    View Slide

  63. JoshuaWarren.com
    DI in Magento 2 is handled via XML
    files
    #NEPHP

    View Slide

  64. JoshuaWarren.com #NEPHP
    di.xml

    type="Magento\Payment\Block\Form" shared="false">



    Magento_SamplePaymentProvider::form/payinstore.phtml





    View Slide

  65. INTERCEPTORS

    View Slide

  66. JoshuaWarren.com
    Plugin system based on the
    interceptor pattern
    #NEPHP

    View Slide

  67. JoshuaWarren.com
    Calls to almost any module can
    be intercepted and altered
    #NEPHP

    View Slide

  68. JoshuaWarren.com
    Vast improvement over the
    rewrite pattern in Magento 1 - no
    more rewrite conflicts
    #NEPHP

    View Slide

  69. JoshuaWarren.com #NEPHP
    di.xml





    View Slide

  70. JoshuaWarren.com
    Sort order defines order if
    multiple plugins intercept the
    same item
    #NEPHP

    View Slide

  71. JoshuaWarren.com
    Possible to intercept before,
    after and around a function
    #NEPHP

    View Slide

  72. JoshuaWarren.com #NEPHP
    ‘Before’ Interceptor
    class Plugin

    {

    public function beforeSetName(\Magento\Catalog\Model\Product $subject, $name)

    {

    return array('(' . $name . ')');

    }

    }

    View Slide

  73. JoshuaWarren.com #NEPHP
    ‘After’ Interceptor
    class Plugin

    {

    public function afterGetName(\Magento\Catalog\Model\Product $subject, $result)

    {

    return '|' . $result . '|';

    }

    }

    View Slide

  74. JoshuaWarren.com #NEPHP
    ‘Around’ Interceptor
    class Plugin

    {

    public function aroundSave(\Magento\Catalog\Model\Product $subject, \Closure $proceed)

    {

    $this->doSomethingBeforeProductIsSaved();

    $returnValue = $proceed();

    if ($returnValue) {

    $this->postProductToFacebook();

    }

    return $returnValue;

    }

    }

    View Slide

  75. SERVICE
    CONTRACTS
    Credit to Allan MacGregor for the Soylent Green joke.

    View Slide

  76. JoshuaWarren.com
    Set of interfaces to define the public
    API of a module
    #NEPHP

    View Slide

  77. JoshuaWarren.com
    This API is the interface provided to
    other modules to access its
    implementation
    #NEPHP

    View Slide

  78. JoshuaWarren.com
    Designed to hide business logic
    behind a stable interface
    #NEPHP

    View Slide

  79. JoshuaWarren.com
    Service contracts + semantic
    versioning = minor releases will not
    break existing code
    #NEPHP

    View Slide

  80. JoshuaWarren.com
    @deprecated = will be removed
    with the next major version release
    #NEPHP

    View Slide

  81. JoshuaWarren.com #NEPHP
    CustomerRepositoryInterface.php

    namespace Magento\Customer\Api;

    /**

    * Customer CRUD interface.

    */

    interface CustomerRepositoryInterface

    {

    /**

    * Create customer.

    *

    * @api

    * @param \Magento\Customer\Api\Data\CustomerInterface $customer

    * @param string $passwordHash

    * @return \Magento\Customer\Api\Data\CustomerInterface

    * @throws \Magento\Framework\Exception\InputException If bad input is provided

    * @throws \Magento\Framework\Exception\State\InputMismatchException If the provided email is already used

    * @throws \Magento\Framework\Exception\LocalizedException

    */

    public function save(\Magento\Customer\Api\Data\CustomerInterface $customer, $passwordHash = null);

    View Slide

  82. JoshuaWarren.com #NEPHP
    CustomerRepositoryInterface.php
    /**

    * Retrieve customer.

    *

    * @api

    * @param string $email

    * @param int|null $websiteId

    * @return \Magento\Customer\Api\Data\CustomerInterface

    * @throws \Magento\Framework\Exception\NoSuchEntityException If customer with the specified email does not exist.

    * @throws \Magento\Framework\Exception\LocalizedException

    */

    public function get($email, $websiteId = null);

    /**

    * Retrieve customer.

    *

    * @api

    * @param int $customerId

    * @return \Magento\Customer\Api\Data\CustomerInterface

    * @throws \Magento\Framework\Exception\NoSuchEntityException If customer with the specified ID does not exist.

    * @throws \Magento\Framework\Exception\LocalizedException

    */

    public function getById($customerId);


    View Slide

  83. JoshuaWarren.com
    Service Contracts include Data
    Interfaces and Service Interfaces
    #NEPHP

    View Slide

  84. JoshuaWarren.com
    Data Interfaces return information
    about data entities
    #NEPHP

    View Slide

  85. JoshuaWarren.com
    Service Interfaces handle business
    logic
    #NEPHP

    View Slide

  86. JoshuaWarren.com
    Three types of service interfaces in
    Magento 2 (so far)
    #NEPHP

    View Slide

  87. JoshuaWarren.com
    Repository Interfaces provide access
    to persistent data entities
    #NEPHP

    View Slide

  88. JoshuaWarren.com
    CustomerRepositoryInterface,
    AddressRepositoryInterface, etc.
    #NEPHP

    View Slide

  89. JoshuaWarren.com
    Repository interfaces contain the
    CRUD operations
    #NEPHP

    View Slide

  90. JoshuaWarren.com
    Management interfaces contain
    management functions not related
    to repositories
    #NEPHP

    View Slide

  91. JoshuaWarren.com
    Validators, createAccount,
    changePassword, etc
    #NEPHP

    View Slide

  92. JoshuaWarren.com
    Metadata interfaces provide meta
    information - primarily about
    custom attributes
    #NEPHP

    View Slide

  93. EXTENDING
    MAGENTO 2

    View Slide

  94. JoshuaWarren.com
    Create your basic module file
    structure
    #NEPHP

    View Slide

  95. JoshuaWarren.com #NEPHP
    App/Code///
    composer.json
    etc/module.xml
    Test/Unit/

    View Slide

  96. JoshuaWarren.com #NEPHP
    Composer.json
    {
    "name": "joshuaswarren/sample-module-minimal",
    "description": "A minimal sample Magento 2 module",
    "type": "magento2-module",
    "version": "1.0.0",
    "license": [
    "OSL-3.0",
    "AFL-3.0"
    ],
    "require": {
    "php": "~5.5.0|~5.6.0",
    "magento/magento-composer-installer": "*"
    },
    "extra": {
    "map": [
    [
    "*",
    "joshuaswarren/SampleMinimal"
    ]
    ]
    }
    }

    View Slide

  97. JoshuaWarren.com #NEPHP
    etc/module.xml




    View Slide

  98. JoshuaWarren.com
    Optional config files in etc:
    acl.xml
    config.xml
    di.xml
    webapi.xml
    #NEPHP

    View Slide

  99. JoshuaWarren.com
    acl.xml defines new items for
    Magento’s ACL system
    #NEPHP

    View Slide

  100. JoshuaWarren.com
    Config.xml adds new
    configuration options
    #NEPHP

    View Slide

  101. JoshuaWarren.com
    Webapi.xml defines items to
    expose via the REST or SOAP APIs
    #NEPHP

    View Slide

  102. JoshuaWarren.com
    Optional subdirectories in etc:
    adminhtml
    frontend
    webapi_rest
    webapi_soap
    #NEPHP

    View Slide

  103. JoshuaWarren.com
    Items in the main etc directory
    apply globally to your extension
    #NEPHP

    View Slide

  104. JoshuaWarren.com
    Items in the 4 subdirectories apply
    only to that area - i.e., adminhtml
    only applies to the Magento
    backend
    #NEPHP

    View Slide

  105. JoshuaWarren.com
    Optional subdirectories:
    API
    Block
    Controller
    Helper
    Model
    #NEPHP

    View Slide

  106. JoshuaWarren.com
    Optional subdirectories:
    Plugin
    Setup
    Ui
    i18n
    view
    #NEPHP

    View Slide

  107. JoshuaWarren.com
    API contains any new service
    contracts your extension adds
    #NEPHP

    View Slide

  108. JoshuaWarren.com
    Block contains any new template
    blocks your extension adds
    #NEPHP

    View Slide

  109. JoshuaWarren.com
    Controller contains your
    extension’s controllers
    #NEPHP

    View Slide

  110. JoshuaWarren.com
    Helper contains any helper
    functions that your extension
    needs
    #NEPHP

    View Slide

  111. JoshuaWarren.com
    Model contains your extension’s
    models
    #NEPHP

    View Slide

  112. JoshuaWarren.com
    Plugin contains any interceptors
    your extension defines
    #NEPHP

    View Slide

  113. JoshuaWarren.com
    Setup contains your database
    migrations using Magento’s setup
    script system
    #NEPHP

    View Slide

  114. JoshuaWarren.com
    UI is for Magento 2’s new Magento
    UI library
    #NEPHP

    View Slide

  115. JoshuaWarren.com
    I18n contains internationalization
    files - CSV files defining the
    translations for your strings
    #NEPHP

    View Slide

  116. JoshuaWarren.com
    View contains the views for your
    extension
    #NEPHP

    View Slide

  117. JoshuaWarren.com
    If your extension doesn’t need
    one of these items, just omit that
    folder
    #NEPHP

    View Slide

  118. JoshuaWarren.com
    Sample: custom shipping method
    to allow for in-store pickup from
    several locations
    #NEPHP

    View Slide

  119. JoshuaWarren.com #NEPHP
    Block/System/Config/Form/Field/Locations.php
    namespace Magento\SampleShippingProvider\Block\System\Config\Form\Field;

    use Magento\Config\Block\System\Config\Form\Field\FieldArray\AbstractFieldArray;

    /**

    * Backend system config array field renderer

    */

    class Locations extends AbstractFieldArray

    {

    /**

    * Initialise columns for 'Store Locations'

    *

    * @return void

    */

    protected function _construct()

    {

    $this->addColumn('title',

    ['label' => __('Title'), 'class' => 'validate-no-empty validate-alphanum-with-spaces']);

    $this->addColumn('street',

    ['label' => __('Street Address'), 'class' => 'validate-no-empty validate-alphanum-with-spaces']);

    $this->addColumn('phone',

    ['label' => __('Phone Number'), 'class' => 'validate-no-empty validate-no-empty validate-phoneStrict']);

    $this->addColumn('message',

    ['label' => __('Message'), 'class' => 'validate-no-empty']);

    $this->_addAfter = false;

    parent::_construct();

    }

    }

    View Slide

  120. JoshuaWarren.com #NEPHP
    Model/Type/Plugin/Onepage.php [1/2]
    namespace Magento\SampleShippingProvider\Model\Type\Plugin;

    use Magento\Checkout\Model\Type\Onepage as CheckoutOnePage;

    use Magento\SampleShippingProvider\Model\Carrier;

    /**

    * Change Shipping Address to selected Store location address

    */

    class Onepage

    {

    /**

    * @var Carrier

    */

    private $carrier;

    /**

    * @param Carrier $carrier

    */

    public function __construct(Carrier $carrier)

    {

    $this->carrier = $carrier;

    }


    View Slide

  121. JoshuaWarren.com #NEPHP
    Model/Type/Plugin/Onepage.php [2/2]
    /**

    * Replace shipping address with pickup location address

    * @param CheckoutOnePage $subject

    * @param array $result

    * @return $this

    */

    public function afterSaveShippingMethod(CheckoutOnePage $subject, array $result)

    {

    if ($result) {

    return $result;

    }

    $quote = $subject->getQuote();

    $shippingAddress = $quote->getShippingAddress();

    $shippingMethod = $shippingAddress->getShippingMethod();

    /**

    * In-Store pickup selected

    * Update Shipping Address

    */

    if (strpos($shippingMethod, $this->carrier->getCarrierCode()) !== false) {

    $locationAddress = $this->carrier->getLocationInfo($shippingMethod);

    $shippingAddress->setCountryId($locationAddress['country_id']);

    $shippingAddress->setRegionId($locationAddress['region_id']);

    $shippingAddress->setPostcode($locationAddress['postcode']);

    $shippingAddress->setCity($locationAddress['city']);

    $shippingAddress->setStreet($locationAddress['street']);

    $shippingAddress->setTelephone($locationAddress['phone']);

    }

    return $result;

    }

    }

    View Slide

  122. JoshuaWarren.com #NEPHP
    Model/Carrier.php
    namespace Magento\SampleShippingProvider\Model;

    use Psr\Log\LoggerInterface;

    use Magento\Framework\App\Config\ScopeConfigInterface;

    use Magento\Store\Model\ScopeInterface;

    use Magento\Shipping\Model\Carrier\AbstractCarrier;

    use Magento\Shipping\Model\Carrier\CarrierInterface;

    use Magento\Shipping\Model\Config;

    use Magento\Shipping\Model\Rate\ResultFactory;

    use Magento\Quote\Model\Quote\Address\RateResult\MethodFactory;

    use Magento\Quote\Model\Quote\Address\RateResult\ErrorFactory;

    /**

    * In-Store Pickup shipping model

    */

    class Carrier extends AbstractCarrier implements CarrierInterface

    {

    /**

    * @var string

    */

    protected $_code = 'storepickup';

    /**

    * @var bool

    */

    protected $_isFixed = true;
    … see https://github.com/magento/magento2-samples/blob/master/sample-module-shipping-provider/Model/Carrier.php 


    View Slide

  123. JoshuaWarren.com
    In model/carrier.php we implement
    all of the functions a shipping
    carrier must have in Magento 2
    #NEPHP

    View Slide

  124. JoshuaWarren.com
    GetAllowedMethods
    CollectRates
    GetLocationInfo
    BuildRateForLocation
    GetLocations
    GetShippingOrigin
    #NEPHP

    View Slide

  125. JoshuaWarren.com #NEPHP
    Etc/Adminhtml/System.xml



    In-Store Pickup


    Enabled

    Magento\Config\Model\Config\Source\Yesno


    Warning: Shipping Origin should be configured to use this method.]]>




    Title


    […]

    View Slide

  126. JoshuaWarren.com #NEPHP
    Etc/Frontend/di.xml






    View Slide

  127. JoshuaWarren.com #NEPHP
    Etc/Config.xml




    1

    Magento\SampleShippingProvider\Model\Carrier

    In-Store Pickup

    This shipping method is not available.





    View Slide

  128. JoshuaWarren.com #NEPHP
    Etc/Module.xml




    View Slide

  129. JoshuaWarren.com #NEPHP
    Composer.json
    {
    "name": "magento/sample-module-shipping-provider",
    "description": "Demonstrate Shipping Provider",
    "type": "magento2-module",
    "version": "1.0.0",
    "license": [
    "OSL-3.0",
    "AFL-3.0"
    ],
    "require": {
    "php": "~5.5.0|~5.6.0",
    "magento/magento-composer-installer": "*",
    "magento/framework": "~0.74",
    "magento/module-store": "~0.74",
    "magento/module-shipping": "~0.74",
    "magento/module-quote": "~0.74",
    "magento/module-checkout": "~0.74"
    },
    "extra": {
    "map": [
    [
    "*",
    "Magento/SampleShippingProvider"
    ]
    ]
    }
    }

    View Slide

  130. JoshuaWarren.com
    We now have a complete,
    functioning in-store-pickup
    shipping extension for Magento 2
    #NEPHP

    View Slide

  131. JoshuaWarren.com
    Take a look at the Test/Unit
    directory on Github for tests for
    this extension
    #NEPHP

    View Slide

  132. JoshuaWarren.com
    Magento 2 unit testing includes
    mocking, fixtures, etc. -
    everything you need for TDD
    #NEPHP

    View Slide

  133. LEARNING
    MORE
    Don’t end up like this guy ->

    View Slide

  134. JoshuaWarren.com
    devdocs.magento.com
    magento.stackexchange.com/questions/tagged/magento2
    #NEPHP

    View Slide

  135. JoshuaWarren.com
    AlanStorm.com
    AlanKent.me
    CoderOnCode.com
    #NEPHP

    View Slide

  136. JoshuaWarren.com
    Upcoming events: Meet Magento
    New York, ZendCon, php[world]
    #NEPHP

    View Slide

  137. JoshuaWarren.com
    Programming With Magento 2 coming to
    amazon.com & phparch.com
    #NEPHP

    View Slide

  138. Keep in
    Touch!
    joind.in/14737
    @JoshuaSWarren
    JoshuaWarren.com
    Mage2DevBook.com

    View Slide

  139. JoshuaWarren.com #NEPHP

    View Slide