Table of Contents for
PHP 7: Real World Application Development

Version ebook / Retour

Cover image for bash Cookbook, 2nd Edition PHP 7: Real World Application Development by Branko Ajzele Published by Packt Publishing, 2016
  1. Cover
  2. Table of Contents
  3. PHP 7: Real World Application Development
  4. PHP 7: Real World Application Development
  5. PHP 7: Real World Application Development
  6. Credits
  7. Preface
  8. What you need for this learning path
  9. Who this learning path is for
  10. Reader feedback
  11. Customer support
  12. 1. Module 1
  13. 1. Building a Foundation
  14. PHP 7 installation considerations
  15. Using the built-in PHP web server
  16. Defining a test MySQL database
  17. Installing PHPUnit
  18. Implementing class autoloading
  19. Hoovering a website
  20. Building a deep web scanner
  21. Creating a PHP 5 to PHP 7 code converter
  22. 2. Using PHP 7 High Performance Features
  23. Understanding the abstract syntax tree
  24. Understanding differences in parsing
  25. Understanding differences in foreach() handling
  26. Improving performance using PHP 7 enhancements
  27. Iterating through a massive file
  28. Uploading a spreadsheet into a database
  29. Recursive directory iterator
  30. 3. Working with PHP Functions
  31. Developing functions
  32. Hinting at data types
  33. Using return value data typing
  34. Using iterators
  35. Writing your own iterator using generators
  36. 4. Working with PHP Object-Oriented Programming
  37. Developing classes
  38. Extending classes
  39. Using static properties and methods
  40. Using namespaces
  41. Defining visibility
  42. Using interfaces
  43. Using traits
  44. Implementing anonymous classes
  45. 5. Interacting with a Database
  46. Using PDO to connect to a database
  47. Building an OOP SQL query builder
  48. Handling pagination
  49. Defining entities to match database tables
  50. Tying entity classes to RDBMS queries
  51. Embedding secondary lookups into query results
  52. Implementing jQuery DataTables PHP lookups
  53. 6. Building Scalable Websites
  54. Creating a generic form element generator
  55. Creating an HTML radio element generator
  56. Creating an HTML select element generator
  57. Implementing a form factory
  58. Chaining $_POST filters
  59. Chaining $_POST validators
  60. Tying validation to a form
  61. 7. Accessing Web Services
  62. Converting between PHP and XML
  63. Creating a simple REST client
  64. Creating a simple REST server
  65. Creating a simple SOAP client
  66. Creating a simple SOAP server
  67. 8. Working with Date/Time and International Aspects
  68. Using emoticons or emoji in a view script
  69. Converting complex characters
  70. Getting the locale from browser data
  71. Formatting numbers by locale
  72. Handling currency by locale
  73. Formatting date/time by locale
  74. Creating an HTML international calendar generator
  75. Building a recurring events generator
  76. Handling translation without gettext
  77. 9. Developing Middleware
  78. Authenticating with middleware
  79. Using middleware to implement access control
  80. Improving performance using the cache
  81. Implementing routing
  82. Making inter-framework system calls
  83. Using middleware to cross languages
  84. 10. Looking at Advanced Algorithms
  85. Using getters and setters
  86. Implementing a linked list
  87. Building a bubble sort
  88. Implementing a stack
  89. Building a binary search class
  90. Implementing a search engine
  91. Displaying a multi-dimensional array and accumulating totals
  92. 11. Implementing Software Design Patterns
  93. Creating an array to object hydrator
  94. Building an object to array hydrator
  95. Implementing a strategy pattern
  96. Defining a mapper
  97. Implementing object-relational mapping
  98. Implementing the Pub/Sub design pattern
  99. 12. Improving Web Security
  100. Filtering $_POST data
  101. Validating $_POST data
  102. Safeguarding the PHP session
  103. Securing forms with a token
  104. Building a secure password generator
  105. Safeguarding forms with a CAPTCHA
  106. Encrypting/decrypting without mcrypt
  107. 13. Best Practices, Testing, and Debugging
  108. Using Traits and Interfaces
  109. Universal exception handler
  110. Universal error handler
  111. Writing a simple test
  112. Writing a test suite
  113. Generating fake test data
  114. Customizing sessions using session_start parameters
  115. A. Defining PSR-7 Classes
  116. Implementing PSR-7 value object classes
  117. Developing a PSR-7 Request class
  118. Defining a PSR-7 Response class
  119. 2. Module 2
  120. 1. Setting Up the Environment
  121. Setting up Debian or Ubuntu
  122. Setting up CentOS
  123. Setting up Vagrant
  124. Summary
  125. 2. New Features in PHP 7
  126. New operators
  127. Uniform variable syntax
  128. Miscellaneous features and changes
  129. Summary
  130. 3. Improving PHP 7 Application Performance
  131. HTTP server optimization
  132. HTTP persistent connection
  133. Content Delivery Network (CDN)
  134. CSS and JavaScript optimization
  135. Full page caching
  136. Varnish
  137. The infrastructure
  138. Summary
  139. 4. Improving Database Performance
  140. Storage engines
  141. The Percona Server - a fork of MySQL
  142. MySQL performance monitoring tools
  143. Percona XtraDB Cluster (PXC)
  144. Redis – the key-value cache store
  145. Memcached key-value cache store
  146. Summary
  147. 5. Debugging and Profiling
  148. Profiling with Xdebug
  149. PHP DebugBar
  150. Summary
  151. 6. Stress/Load Testing PHP Applications
  152. ApacheBench (ab)
  153. Siege
  154. Load testing real-world applications
  155. Summary
  156. 7. Best Practices in PHP Programming
  157. Test-driven development (TDD)
  158. Design patterns
  159. Service-oriented architecture (SOA)
  160. Being object-oriented and reusable always
  161. PHP frameworks
  162. Version control system (VCS) and Git
  163. Deployment and Continuous Integration (CI)
  164. Summary
  165. A. Tools to Make Life Easy
  166. Git – A version control system
  167. Grunt watch
  168. Summary
  169. B. MVC and Frameworks
  170. Laravel
  171. Lumen
  172. Apigility
  173. Summary
  174. 3. Module 3
  175. 1. Ecosystem Overview
  176. Summary
  177. 2. GoF Design Patterns
  178. Structural patterns
  179. Behavioral patterns
  180. Summary
  181. 3. SOLID Design Principles
  182. Open/closed principle
  183. Liskov substitution principle
  184. Interface Segregation Principle
  185. Dependency inversion principle
  186. Summary
  187. 4. Requirement Specification for a Modular Web Shop App
  188. Wireframing
  189. Defining a technology stack
  190. Summary
  191. 5. Symfony at a Glance
  192. Creating a blank project
  193. Using Symfony console
  194. Controller
  195. Routing
  196. Templates
  197. Forms
  198. Configuring Symfony
  199. The bundle system
  200. Databases and Doctrine
  201. Testing
  202. Validation
  203. Summary
  204. 6. Building the Core Module
  205. Dependencies
  206. Implementation
  207. Unit testing
  208. Functional testing
  209. Summary
  210. 7. Building the Catalog Module
  211. Dependencies
  212. Implementation
  213. Unit testing
  214. Functional testing
  215. Summary
  216. 8. Building the Customer Module
  217. Dependencies
  218. Implementation
  219. Unit testing
  220. Functional testing
  221. Summary
  222. 9. Building the Payment Module
  223. Dependencies
  224. Implementation
  225. Unit testing
  226. Functional testing
  227. Summary
  228. 10. Building the Shipment Module
  229. Dependencies
  230. Implementation
  231. Unit testing
  232. Functional testing
  233. Summary
  234. 11. Building the Sales Module
  235. Dependencies
  236. Implementation
  237. Unit testing
  238. Functional testing
  239. Summary
  240. 12. Integrating and Distributing Modules
  241. Understanding GitHub
  242. Understanding Composer
  243. Understanding Packagist
  244. Summary
  245. Bibliography
  246. Index

Chaining $_POST filters

Proper filtering and validation is a common problem when processing data submitted by users from an online form. It is arguably also the number one security vulnerability for a website. Furthermore, it can be quite awkward to have the filters and validators scattered all over the application. A chaining mechanism would resolve these issues neatly, and would also allow you to exert control over the order in which the filters and validators are processed.

How to do it...

  1. There is a little-known PHP function, filter_input_array(), that, at first glance, seems well suited for this task. Looking more deeply into its functionality, however, it soon becomes apparent that this function was designed in the early days, and is not up to modern requirements for protection against attack and flexibility. Accordingly, we will instead present a much more flexible mechanism based on an array of callbacks performing filtering and validation.

    Note

    The difference between filtering and validation is that filtering can potentially remove or transform values. Validation, on the other hand, tests data using criteria appropriate to the nature of the data, and returns a boolean result.

  2. In order to increase flexibility, we will make our base filter and validation classes relatively light. By this, we mean not defining any specific filters or validation methods. Instead, we will operate entirely on the basis of a configuration array of callbacks. In order to ensure compatibility in filtering and validation results, we will also define a specific result object, Application\Filter\Result.
  3. The primary function of the Result class will be to hold a $item value, which would be the filtered value or a boolean result of validation. Another property, $messages, will hold an array of messages populated during the filtering or validation operation. In the constructor, the value supplied for $messages is formulated as an array. You might observe that both properties are defined public. This is to facilitate ease of access:
    namespace Application\Filter;
    
    class Result
    {
      
      public $item;  // (mixed) filtered data | (bool) result of validation
      public $messages = array();  // [(string) message, (string) message ]
      
      public function __construct($item, $messages)
      {
        $this->item = $item;
        if (is_array($messages)) {
            $this->messages = $messages;
        } else {
            $this->messages = [$messages];
        }
      }
  4. We also define a method that allows us to merge this Result instance with another. This is important as at some point we will be processing the same value through a chain of filters. In such a case, we want the newly filtered value to overwrite the existing one, but we want the messages to be merged:
    public function mergeResults(Result $result)
    {
      $this->item = $result->item;
      $this->mergeMessages($result);
    }
    
    public function mergeMessages(Result $result)
    {
      if (isset($result->messages) && is_array($result->messages)) {
        $this->messages = array_merge($this->messages, $result->messages);
      }
    }
  5. Finally, to finish the methods for this class, we add a method that merges validation results. The important consideration here is that any value of FALSE, up or down the validation chain, must cause the entire result to be FALSE:
    public function mergeValidationResults(Result $result)
    {
      if ($this->item === TRUE) {
        $this->item = (bool) $result->item;
      }
      $this->mergeMessages($result);
      }
    
    }
  6. Next, to make sure that the callbacks produce compatible results, we will define an Application\Filter\CallbackInterface interface. You will note that we are taking advantage of the PHP 7 ability to data type the return value to ensure that we are getting a Result instance in return:
    namespace Application\Filter;
    interface CallbackInterface
    {
      public function __invoke ($item, $params) : Result;
    }
  7. Each callback should reference the same set of messages. Accordingly, we define a Application\Filter\Messages class with a series of static properties. We provide methods to set all messages, or just one message. The $messages property has been made public for easier access:
    namespace Application\Filter;
    class Messages
    {
      const MESSAGE_UNKNOWN = 'Unknown';
      public static $messages;
      public static function setMessages(array $messages)
      {
        self::$messages = $messages;
      }
      public static function setMessage($key, $message)
      {
        self::$messages[$key] = $message;
      }
      public static function getMessage($key)
      {
        return self::$messages[$key] ?? self::MESSAGE_UNKNOWN;
      }
    }
  8. We are now in a position to define a Application\Web\AbstractFilter class that implements core functionality. As mentioned previously, this class will be relatively lightweight and we do not need to worry about specific filters or validators as they will be supplied through configuration. We use the UnexpectedValueException class, provided as part of the PHP 7 Standard PHP Library (SPL), in order to throw a descriptive exception in case one of the callbacks does not implement CallbackInterface:
    namespace Application\Filter;
    use UnexpectedValueException;
    abstract class AbstractFilter
    {
      // code described in the next several bullets
  9. First, we define useful class constants that hold various housekeeping values. The last four shown here control the format of messages to be displayed, and how to describe missing data:
    const BAD_CALLBACK = 'Must implement CallbackInterface';
    const DEFAULT_SEPARATOR = '<br>' . PHP_EOL;
    const MISSING_MESSAGE_KEY = 'item.missing';
    const DEFAULT_MESSAGE_FORMAT = '%20s : %60s';
    const DEFAULT_MISSING_MESSAGE = 'Item Missing';
  10. Next, we define core properties. $separator is used in conjunction with filtering and validation messages. $callbacks represents the array of callbacks that perform filtering and validation. $assignments map data fields to filters and/or validators. $missingMessage is represented as a property so that it can be overwritten (that is, for multi-language websites). Finally, $results is an array of Application\Filter\Result objects and is populated by the filtering or validation operation:
    protected $separator;    // used for message display
    protected $callbacks;
    protected $assignments;
    protected $missingMessage;
    protected $results = array();
  11. At this point, we can build the __construct() method. Its main function is to set the array of callbacks and assignments. It also either sets values or accepts defaults for the separator (used in message display), and the missing message:
    public function __construct(array $callbacks, array $assignments, 
                                $separator = NULL, $message = NULL)
    {
      $this->setCallbacks($callbacks);
      $this->setAssignments($assignments);
      $this->setSeparator($separator ?? self::DEFAULT_SEPARATOR);
      $this->setMissingMessage($message 
                               ?? self::DEFAULT_MISSING_MESSAGE);
    }
  12. Next, we define a series of methods that allow us to set or remove callbacks. Notice that we allow the getting and setting of a single callback. This is useful if you have a generic set of callbacks, and need to modify just one. You will also note that setOneCall() checks to see if the callback implements CallbackInterface. If it does not, an UnexpectedValueException is thrown:
    public function getCallbacks()
    {
      return $this->callbacks;
    }
    
    public function getOneCallback($key)
    {
      return $this->callbacks[$key] ?? NULL;
    }
    
    public function setCallbacks(array $callbacks)
    {
      foreach ($callbacks as $key => $item) {
        $this->setOneCallback($key, $item);
      }
    }
    
    public function setOneCallback($key, $item)
    {
      if ($item instanceof CallbackInterface) {
          $this->callbacks[$key] = $item;
      } else {
          throw new UnexpectedValueException(self::BAD_CALLBACK);
      }
    }
    
    public function removeOneCallback($key)
    {
      if (isset($this->callbacks[$key])) 
      unset($this->callbacks[$key]);
    }
  13. Methods for results processing are quite simple. For convenience, we added getItemsAsArray(), otherwise getResults() will return an array of Result objects:
    public function getResults()
    {
      return $this->results;
    }
    
    public function getItemsAsArray()
    {
      $return = array();
      if ($this->results) {
        foreach ($this->results as $key => $item) 
        $return[$key] = $item->item;
      }
      return $return;
    }
  14. Retrieving messages is just a matter of looping through the array of $this ->results and extracting the $messages property. For convenience, we also added getMessageString() with some formatting options. To easily produce an array of messages, we use the PHP 7 yield from syntax. This has the effect of turning getMessages() into a delegating generator. The array of messages becomes a sub-generator:
    public function getMessages()
    {
      if ($this->results) {
          foreach ($this->results as $key => $item) 
          if ($item->messages) yield from $item->messages;
      } else {
          return array();
      }
    }
    
    public function getMessageString($width = 80, $format = NULL)
    {
      if (!$format)
      $format = self::DEFAULT_MESSAGE_FORMAT . $this->separator;
      $output = '';
      if ($this->results) {
        foreach ($this->results as $key => $value) {
          if ($value->messages) {
            foreach ($value->messages as $message) {
              $output .= sprintf(
                $format, $key, trim($message));
            }
          }
        }
      }
      return $output;
    }
  15. Lastly, we define a mixed group of useful getters and setters:
      public function setMissingMessage($message)
      {
        $this->missingMessage = $message;
      }
      public function setSeparator($separator)
      {
        $this->separator = $separator;
      }
      public function getSeparator()
      {
        return $this->separator;
      }
      public function getAssignments()
      {
        return $this->assignments;
      }
      public function setAssignments(array $assignments)
      {
        $this->assignments = $assignments;
      }
      // closing bracket for class AbstractFilter
    }
  16. Filtering and validation, although often performed together, are just as often performed separately. Accordingly, we define discrete classes for each. We'll start with Application\Filter\Filter. We make this class extend AbstractFilter in order to provide the core functionality described previously:
    namespace Application\Filter;
    class Filter extends AbstractFilter
    {
      // code
    }
  17. Within this class we define a core process() method that scans an array of data and applies filters as per the array of assignments. If there are no assigned filters for this data set, we simply return NULL:
    public function process(array $data)
    {
      if (!(isset($this->assignments) 
          && count($this->assignments))) {
            return NULL;
      }
  18. Otherwise, we initialize $this->results to an array of Result objects where the $item property is the original value from $data, and the $messages property is an empty array:
    foreach ($data as $key => $value) {
      $this->results[$key] = new Result($value, array());
    }
  19. We then make a copy of $this->assignments and check to see if there are any global filters (identified by the '*' key. If so, we run processGlobal() and then unset the '*' key:
    $toDo = $this->assignments;
    if (isset($toDo['*'])) {
      $this->processGlobalAssignment($toDo['*'], $data);
      unset($toDo['*']);
    }
  20. Finally, we loop through any remaining assignments, calling processAssignment():
    foreach ($toDo as $key => $assignment) {
      $this->processAssignment($assignment, $key);
    }
  21. As you will recall, each assignment is keyed to the data field, and represents an array of callbacks for that field. Thus, in processGlobalAssignment() we need to loop through the array of callbacks. In this case, however, because these assignments are global, we also need to loop through the entire data set, and apply each global filter in turn:
    protected function processGlobalAssignment($assignment, $data)
    {
      foreach ($assignment as $callback) {
        if ($callback === NULL) continue;
        foreach ($data as $k => $value) {
          $result = $this->callbacks[$callback['key']]($this->results[$k]->item,
          $callback['params']);
          $this->results[$k]->mergeResults($result);
        }
      }
    }

    Note

    The tricky bit is this line of code:

    $result = $this->callbacks[$callback['key']]($this ->results[$k]->item, $callback['params']);

    Remember, each callback is actually an anonymous class that defines the PHP magic __invoke() method. The arguments supplied are the actual data item to be filtered, and an array of parameters. By running $this->callbacks[$callback['key']]() we are in fact magically calling __invoke().

  22. When we define processAssignment(), in a manner akin to processGlobalAssignment(), we need to execute each remaining callback assigned to each data key:
      protected function processAssignment($assignment, $key)
      {
        foreach ($assignment as $callback) {
          if ($callback === NULL) continue;
          $result = $this->callbacks[$callback['key']]($this->results[$key]->item, 
                                     $callback['params']);
          $this->results[$key]->mergeResults($result);
        }
      }
    }  // closing brace for Application\Filter\Filter

    Note

    It is important that any filtering operation that alters the original user-supplied data should display a message indicating that a change was made. This can become part of an audit trail to safeguard you against potential legal liability when a change is made without user knowledge or consent.

How it works...

Create an Application\Filter folder. In this folder, create the following class files, using code from the preceding steps:

Application\Filter\* class file

Code described in these steps

Result.php

3 - 5

CallbackInterface.php

6

Messages.php

7

AbstractFilter.php

8 - 15

Filter.php

16 - 22

Next, take the code discussed in step 5, and use it to configure an array of messages in a chap_06_post_data_config_messages.php file. Each callback references the Messages::$messages property. Here is a sample configuration:

<?php
use Application\Filter\Messages;
Messages::setMessages(
  [
    'length_too_short' => 'Length must be at least %d',
    'length_too_long'  => 'Length must be no more than %d',
    'required'         => 'Please be sure to enter a value',
    'alnum'            => 'Only letters and numbers allowed',
    'float'            => 'Only numbers or decimal point',
    'email'            => 'Invalid email address',
    'in_array'         => 'Not found in the list',
    'trim'             => 'Item was trimmed',
    'strip_tags'       => 'Tags were removed from this item',
    'filter_float'     => 'Converted to a decimal number',
    'phone'            => 'Phone number is [+n] nnn-nnn-nnnn',
    'test'             => 'TEST',
    'filter_length'    => 'Reduced to specified length',
  ]
);

Next, create a chap_06_post_data_config_callbacks.php callback configuration file that contains configuration for filtering callbacks, as described in step 4. Each callback should follow this generic template:

'callback_key' => new class () implements CallbackInterface 
{
  public function __invoke($item, $params) : Result
  {
    $changed  = array();
    $filtered = /* perform filtering operation on $item */
    if ($filtered !== $item) $changed = Messages::$messages['callback_key'];
    return new Result($filtered, $changed);
  }
}

The callbacks themselves must implement the interface and return a Result instance. We can take advantage of the PHP 7 anonymous class capability by having our callbacks return an anonymous class that implements CallbackInterface. Here is how an array of filtering callbacks might look:

use Application\Filter\ { Result, Messages, CallbackInterface };
$config = [ 'filters' => [
  'trim' => new class () implements CallbackInterface 
  {
    public function __invoke($item, $params) : Result
    {
      $changed  = array();
      $filtered = trim($item);
      if ($filtered !== $item) 
      $changed = Messages::$messages['trim'];
      return new Result($filtered, $changed);
    }
  },
  'strip_tags' => new class () 
  implements CallbackInterface 
  {
    public function __invoke($item, $params) : Result
    {
      $changed  = array();
      $filtered = strip_tags($item);
      if ($filtered !== $item)     
      $changed = Messages::$messages['strip_tags'];
      return new Result($filtered, $changed);
    }
  },
  // etc.
]
];

For test purposes, we will use the prospects table as a target. Instead of providing data from $_POST, we will construct an array of good and bad data:

How it works...

You can now create a chap_06_post_data_filtering.php script that sets up autoloading, includes the messages and callbacks configuration files:

<?php
require __DIR__ . '/../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/..');
include __DIR__ . '/chap_06_post_data_config_messages.php';
include __DIR__ . '/chap_06_post_data_config_callbacks.php';

You then need to define assignments that represent a mapping between the data fields and filter callbacks. Use the * key to define a global filter that applies to all data:

$assignments = [
  '*'   => [ ['key' => 'trim', 'params' => []], 
          ['key' => 'strip_tags', 'params' => []] ],
  'first_name'  => [ ['key' => 'length', 
   'params' => ['length' => 128]] ],
  'last_name'  => [ ['key' => 'length', 
   'params' => ['length' => 128]] ],
  'city'          => [ ['key' => 'length', 
   'params' => ['length' => 64]] ],
  'budget'     => [ ['key' => 'filter_float', 'params' => []] ],
];

Next, define good and bad test data:

$goodData = [
  'first_name'      => 'Your Full',
  'last_name'       => 'Name',
  'address'         => '123 Main Street',
  'city'            => 'San Francisco',
  'state_province'  => 'California',
  'postal_code'     => '94101',
  'phone'           => '+1 415-555-1212',
  'country'         => 'US',
  'email'           => 'your@email.address.com',
  'budget'          => '123.45',
];
$badData = [
  'first_name'      => 'This+Name<script>bad tag</script>Valid!',
  'last_name'       => 'ThisLastNameIsWayTooLongAbcdefghijklmnopqrstuvwxyz0123456789Abcdefghijklmnopqrstuvwxyz0123456789Abcdefghijklmnopqrstuvwxyz0123456789Abcdefghijklmnopqrstuvwxyz0123456789',
  //'address'       => '',    // missing
  'city'            => '  ThisCityNameIsTooLong012345678901234567890123456789012345678901234567890123456789  ',
  //'state_province'=> '',    // missing
  'postal_code'     => '!"£$%^Non Alpha Chars',
  'phone'           => ' 12345 ',
  'country'         => 'XX',
  'email'           => 'this.is@not@an.email',
  'budget'          => 'XXX',
];

Finally, you can create an Application\Filter\Filter instance, and test the data:

$filter = new Application\Filter\Filter(
$config['filters'], $assignments);
$filter->setSeparator(PHP_EOL);
  $filter->process($goodData);
echo $filter->getMessageString();
  var_dump($filter->getItemsAsArray());

$filter->process($badData);
echo $filter->getMessageString();
var_dump($filter->getItemsAsArray());

Processing good data produces no messages other than one indicating that the value for the float field was converted from string to float. The bad data, on the other hand, produces the following output:

How it works...

You will also notice that tags were removed from first_name, and that both last_name and city were truncated.

There's more...

The filter_input_array() function takes two arguments: the input source (in the form of a pre-defined constant used to indicate one of the $_* PHP super-globals, that is, $_POST), and an array of matching field definitions as keys and filters or validators as values. This function performs not only filtering operations, but validation as well. The flags labeled sanitize are actually filters.

See also

Documentation and examples of filter_input_array() can be found at http://php.net/manual/en/function.filter-input-array.php. You might also have a look at the different types of filters that are available on http://php.net/manual/en/filter.filters.php.