πŸ“œ ⬆️ ⬇️

DIY mini framework

Recently, after reading about the Silex mini-framework , I thought: what is so complicated about it? I tried to write something similar and it turned out pretty easy.

These mini-frameworks are usually based on the following rules for mod_rewrite:
Copy Source | Copy HTML < IfModule mod_rewrite . c > RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [QSA,L] </ IfModule >
  1. Copy Source | Copy HTML < IfModule mod_rewrite . c > RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [QSA,L] </ IfModule >
  2. Copy Source | Copy HTML < IfModule mod_rewrite . c > RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [QSA,L] </ IfModule >
  3. Copy Source | Copy HTML < IfModule mod_rewrite . c > RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [QSA,L] </ IfModule >
  4. Copy Source | Copy HTML < IfModule mod_rewrite . c > RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [QSA,L] </ IfModule >
  5. Copy Source | Copy HTML < IfModule mod_rewrite . c > RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [QSA,L] </ IfModule >
  6. Copy Source | Copy HTML < IfModule mod_rewrite . c > RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [QSA,L] </ IfModule >
  7. Copy Source | Copy HTML < IfModule mod_rewrite . c > RewriteEngine On RewriteBase / RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php [QSA,L] </ IfModule >

They redirect any request to index.php, in which the framework is connected and this request is processed with a callback function call. So first, let's write a simple function to process the address:
Copy Source | Copy HTML
  1. function Request ( $ path , $ callback )
  2. {
  3. if ( $ path == $ _SERVER [ 'REQUEST_URI' ]) return call_user_func ( $ callback );
  4. }

When calling, the function checks the address to which the user has addressed, and if it matches the one passed into the function, it calls the callback. This can already be considered the basic functionality. However, there is no one thing to do - extracting variables from an address. In addition, the function finds the addresses example.com/something and example.com/something different. This can be solved by parsing the address and using array_filter:
Copy Source | Copy HTML
  1. function array_filter_callback_no_empty_str ( $ value )
  2. {
  3. return $ value ! = '' ;
  4. }
  5. function Request ( $ path , $ callback )
  6. {
  7. // Request variables for passing to callback
  8. $ args = array ();
  9. // Split the address to which the user has addressed (URI), into parts
  10. $ uri = explode ( '/' , $ _SERVER [ 'REQUEST_URI' ]);
  11. // Do the same with the query path.
  12. $ path = explode ( '/' , $ path );
  13. // Remove the empty parts of both arrays
  14. $ uri = array_values ​​(array_filter ( $ uri , array_filter_callback_no_empty_str));
  15. $ path = array_values ​​(array_filter ( $ path , array_filter_callback_no_empty_str));
  16. // If the number of parts in the URI and the path is different, exit
  17. if (count ( $ uri )! = count ( $ path ))
  18. return false ;
  19. // Pass through all parts of the request path
  20. for ( $ i = 0 ; $ i <count ( $ path ); $ i ++)
  21. {
  22. // Check if this part of the path is variable
  23. // Path variables are written in braces, which is what the regular expression checks.
  24. if (preg_match ( '| ^ \ {(. *) \} $ |' , $ path [ $ i ], $ match ))
  25. {
  26. // If so, add this variable to the array
  27. $ args [ $ match [ 1 ]] = $ uri [ $ i ];
  28. }
  29. else
  30. {
  31. // If the request part is not a variable, simply compare the URI with the request
  32. // If they do not match - exit
  33. if ( $ uri [ $ i ]! = $ path [ $ i ])
  34. return false ;
  35. }
  36. }
  37. // After all the checks we call callback, passing it an array with the request variables
  38. return call_user_func_array ( $ callback , $ args );
  39. }

Now you can write code like this:
Copy Source | Copy HTML
  1. function Hello ( $ who )
  2. {
  3. print "Hello, $ who" ;
  4. }
  5. Request ( '/ hello / {who}' , Hello);

And now let's glance in the direction of Silex and see what we lack. First of all, these are assertions (checking the path variable against a regular expression), checking the method of the call (GET, POST, PUT, etc.) and the object-oriented model. The first two add is quite simple, you only need to add a couple of checks:
Copy Source | Copy HTML
  1. function Request ( $ method , $ path , $ callback , $ asserts = array ())
  2. {
  3. // Check the server access method.
  4. // If a specific method is specified in the request and it does not match the one that was used, exit
  5. if ( $ method ! = '' && strtolower ( $ _SERVER [ 'REQUEST_METHOD' ])! = $ method )
  6. return false ;
  7. <...>
  8. // Now check if the variable is part of the path.
  9. if (preg_match ( '| ^ \ {(. *) \} $ |' , $ path [$ i], $ match ))
  10. {
  11. // If it is, then see if there is a regular expression to check it,
  12. // and if there is, then we check the corresponding part of the URI for compliance
  13. if (! isset ( $ asserts [ $ match [ 1 ]]) || preg_match ( $ asserts [ $ match [ 1 ]], $ uri [$ i]))
  14. {
  15. // If everything is correct, add this variable along with its value to the array
  16. $ args [ $ match [ 1 ]] = $ uri [$ i];
  17. }
  18. else
  19. {
  20. // If the value does not match the regular expression, exit
  21. return false ;
  22. }
  23. }
  24. <...>
  25. }

However, the second is also quite easy to do. We will do the transfer of $ method, $ path, $ callback in the constructor, write a separate wrapper function for asserts, and push the entire work into the run () function, not forgetting to substitute $ this-> for the above variables:
Copy Source | Copy HTML
  1. class Request
  2. {
  3. public $ method ; // Request Method (GET, POST, PUT, etc.)
  4. public $ path ; // Request Path
  5. public $ callback ; // Callback
  6. public $ asserts = array (); // Regular expressions for checking path variables
  7. // class constructor
  8. public function __construct ( $ method , $ path , $ callback )
  9. {
  10. $ this -> method = strtolower ( $ method );
  11. $ this -> path = $ path ;
  12. $ this -> callback = $ callback ;
  13. }
  14. // Add re regular expression to check the path variable named
  15. public function assert ( $ name , $ re )
  16. {
  17. $ this -> asserts [ $ name ] = $ re ;
  18. // Return the current instance class
  19. // This allows you to write similar code: $ reg-> assert ('id', '| ^ \ d + $ |') -> run ();
  20. return $ this ;
  21. }
  22. // Request processing function
  23. public function run ()
  24. {
  25. <...>
  26. }
  27. }

It seems to be all beautiful, except for two moments. The first moment is purely aesthetic - now, to process one request, you need to write two lines of code:
Copy Source | Copy HTML
  1. $ req = new Request ( '/ user / {id}' , UserProfile);
  2. $ req -> assert ( '| ^ \ d + $ |' ) -> run ();

But fortunately, PHP allows you to use the same names for classes and functions, so let's write a wrapper function that will create and return an instance of the class:
Copy Source | Copy HTML
  1. function Request ( $ method , $ path , $ callback )
  2. {
  3. return new Request ( $ method , $ path , $ callback );
  4. }

Now you can write everything again compactly and conveniently:
Copy Source | Copy HTML
  1. Request ( '/ user / {id}' , UserProfile) -> assert ( '| ^ \ d + $ |' ) -> run ();

The second unpleasant moment is the absence of any single center where requests will be stored and processed. Why do we call the run () function all the time? But even if you make such a single center, it will be much easier if new requests are added to its queue. Plus, there shouldn't be two centers, so you need to implement a singleton. So, write it already!
Copy Source | Copy HTML
  1. class Application
  2. {
  3. public $ requests = array ();
  4. /// ---
  5. // Implement singleton
  6. protected static $ instance ;
  7. private function __construct ()
  8. {
  9. }
  10. private function __clone ()
  11. {
  12. }
  13. public static function getInstance ()
  14. {
  15. if (! is_object (self :: $ instance ))
  16. {
  17. self :: $ instance = new self;
  18. }
  19. return self :: $ instance ;
  20. }
  21. public static function init ()
  22. {
  23. self :: getInstance ();
  24. }
  25. /// ---
  26. // Internal function to handle all requests
  27. private function i_run ()
  28. {
  29. foreach ( $ this -> requests as & $ request )
  30. {
  31. $ done = $ request -> run ( $ params );
  32. if ( $ done ) return true ;
  33. }
  34. return false ;
  35. }
  36. // External static wrapper function over i_run
  37. // Needed only for aesthetics: Application :: run () looks prettier than Application :: getInstance () -> run ()
  38. public static function run ()
  39. {
  40. return Application :: getInstance () -> i_run ();
  41. }
  42. }

Also slightly change the class Request:
Copy Source | Copy HTML
  1. class Request
  2. {
  3. <...>
  4. // class constructor
  5. public function __construct ( $ method , $ path , $ callback )
  6. {
  7. $ this -> method = strtolower ( $ method );
  8. $ this -> path = $ path ;
  9. $ this -> callback = $ callback ;
  10. // Add this request to the queue to the Application
  11. Application :: getInstance () -> requests [] = $ this ;
  12. }
  13. <...>
  14. public function run ()
  15. {
  16. <...>
  17. // After all the checks we call callback, passing it an array with the request variables
  18. $ result = call_user_func_array ( $ this -> callback, $ this -> args);
  19. // If the callback returned a boolean value, return it.
  20. if (is_bool ( $ result ))
  21. return $ result ;
  22. // Otherwise, return true
  23. else
  24. return true ;
  25. }
  26. }

The latter is necessary in order to be able to use multiple requests to process a single path. If the callback returns false, then the following requests will be processed, otherwise everything will end.
In general, using our mini-framework looks quite nice and simple:
Copy Source | Copy HTML
  1. new Application ();
  2. Request ( '/ user / {id}' , UserProfile) -> assert ( '| ^ \ d + $ |' ) -> run ();
  3. Application :: run ();

However, everything can be simplified even more. Among the PHP settings, there are two great options: auto_prepend_file and auto_append_file, which allow you to enable payload PHP scripts before and after the execution of the main script. We can bring the framework into a separate file and connect it using these functions. At the first connection, we will declare the classes and create an Application object, and at the second - call Application :: run (). You can determine if a script is being run for the first time or not by checking whether the Application class exists or the Request class exists:
Copy Source | Copy HTML
  1. if (! class_exists ( 'Application' ))
  2. {
  3. // If the Application class is not yet declared, the script launches for the first time.
  4. class Request
  5. {
  6. <...>
  7. }
  8. function Request ( $ method , $ path , $ callback )
  9. {
  10. <...>
  11. }
  12. class Application
  13. {
  14. <...>
  15. }
  16. // Initialize the Application,
  17. Application :: init ();
  18. }
  19. else
  20. {
  21. // The script does not run for the first time.
  22. Application :: run ();
  23. }

Thus, we got rid of two β€œextra” lines.

This is how you can write a more or less functional mini-framework. It does not reach Silex, but it is quite possible to use it. The full source code (slightly modified) and an example of usage are on http://fw.nizarium.com/ , which works on the same mini-framework.
Please do not consider this as something serious. This is just an example, originally written purely for himself. If there are any errors in it, I am ready to correct them.

')

Source: https://habr.com/ru/post/118237/


All Articles