The custom route doesn't work correctly and always routes to user.htm
index.php
$routes = [
"/" => "index.htm",
"/user/id=#id" => "user.htm","/user/#id" => "user.htm",
];
foreach ($routes as $path => $file) {
$f3->route("GET ".$path,
function($f3){
global $file,$path;
echo View::instance()->render($file);
}
);
}
try this:
$routes = [
"/" => "index.htm",
"/user/id=#id" => "user.htm",
"/user/#id" => "user.htm",
];
foreach ($routes as $path => $file)
{
$f3->route("GET " . $path,
function ($f3) use ($file)
{
echo View::instance()->render($file);
}
);
}
The answer from Bryan Velastegui is the correct one. But here's why your code didn't work:
$f3->route() maps each route URI to a function (called the "route handler"), without executing it.
the foreach loop stores successively the following values into the $file variable: index.html, user.htm and user.htm (again). Therefore, at the end of the loop, $file holds user.htm.
once you call $f3->run(), the framework executes the route handler matching the current route, which itself refers to the global $file variable, holding user.htm.
Generally, you shouldn't be using the global keyword. That will just create unexpected issues, just as the one you've faced. Also that doesn't help for code maintainability.
I advise you to read the docs about the use keyword to understand how Bryan Velastegui's code is working.
Related
I'm a Frontend developer and I've decided to expand my knowledge learning PHP. I'm still learning the syntax / ways of accomplishing stuff so please bear with me.
As I began working on my first PHP project I realised that I needed to create a path map just to keep things clean and DRY.
This is what my php file structure looks like by now:
That is easy to represent using a JSON like data structure, but I've found a embarrassing hard time trying to achieve that using PHP data types ( I'm still learning the syntax / ways of doing things ).
I've been reading a bit and I decided to use some associative arrays,
I've come up with this solution, which works but I wanted to check if a simpler solution is possible. ( I'm aiming to learn the best practices of PHP )
See:
$paths = array(
'dirs' => array(
'base' => '/php/'
)
);
$paths['dirs']['common'] = $paths['dirs']['base'] . 'common/';
$paths['dirs']['home'] = $paths['dirs']['base'] . 'home/';
$paths['files'] = array(
'home' => $paths['dirs']['home'] . 'home.php',
'header' => $paths['dirs']['common'] . 'header.php',
'scripts' => $paths['dirs']['common'] . 'scripts.php',
'footer' => $paths['dirs']['common'] . 'footer.php',
'core' => $paths['dirs']['base'] . 'core.php',
'business-variables' => $paths['dirs']['base'] . 'business-variables.php'
);
Am I doing bad practices here?
Is there a better / simpler / standard way of doing this?
If you need to work with files - the best and easiest way is use DirectoryIterator. In this case each file will be object and you can use his methods. Here a small example:
// or '/php/' in your case
$path = '/';
foreach (new DirectoryIterator($path) as $file) {
if ($file->isDot()) continue;
if ($file->isDir()) {
print $file->getFilename() . '<br/>';
}
}
Provided you can't just read the folder structure using DirectoryIterator...
For readability I'd do something like this:
class FileStructure {
const BASE = '/php/';
const COMMON = 'common/%s';
const HOME = 'home/%s';
public function Paths(){
return array(
'home' => $this->Home('home.php'),
'header' => $this->Common('header.php')
);
}
private function Common($file = null){
return sprintf(static::BASE.static::COMMON, $file);
}
private function Home($file = null){
return sprintf(static::BASE.static::HOME, $file);
}
}
I'm writing a router for my PHP MVC application, and I currently need to find a way to use matches in a route as variables for controllers and actions.
For example, if I have the following route: /users/qub1/home
I would like to use a regex similar to this: \/users\/(?!/).*\/(?!/).*
Then I would like to specify the action like this: $2 (in the example, this would be home)
And the parameter to pass to the action like this: $1 (in the example, this would be qub1).
This would then execute code similar to this:
$controller = new UsersController();
$controller->$2($1);
Configured routes are stored as such:
public function setRoute($route, $regex = false, $controller = 'Index', $action = 'index', $parameters = array()) {
if(!$regex) {
$route = preg_quote($route, '/');
}
$this->routes[] = [
'route' => $route,
'controller' => $controller,
'action' => $action,
'parameters' => $parameters
];
}
Where the above example would be stored like this: $router->setRoute('\/users\/(?!/).*\/(?!/).*', true, 'User', '$2', [$1]);
So essentially, I want to use matched groups from one regex expression as variables to replace inside another regex expression (if that makes sense).
I hope I've described my problem accurately enough. Thanks for the help.
EDIT:
The code I'm currently using to parse routes (it doesn't work, but it should illustrate what I'm trying to achieve):
public function executeRoute($route) {
// Loop over available routes
foreach($this->routes as $currentRoute) {
// Check if the current route matches the provided route
if(preg_match('/^' . $currentRoute['route'] . '$/', '/' . $route, $matches)) {
// If it matches, perform the current route's action
// Define names
$controllerClass = preg_replace('\$.*\d', $matches[str_replace('$', '', '$1')], ucfirst($currentRoute['controller'] . 'Controller'));
$actionMethod = preg_replace('\$.*\d', $matches[str_replace('$', '', '$1')], strtolower($currentRoute['action']) . 'Action');
$parameters = preg_replace('\$.*\d', $matches[str_replace('$', '', '$1')], join(', ', $currentRoute['parameters']));
// Create the controller
$controller = new $controllerClass();
$controller->$actionMethod($parameters);
// Return
return;
}
}
}
While I am not sure that it is a very well designed approach, it is doable. This is the code that replaces yours within the if:
// you already specify the controller name, so no need for replacing
$controllerClass = ucfirst($currentRoute['controller'] . 'Controller');
// also here, no need to replace. You just need to get the right element from the array
$actionMethod = strtolower($matches[ltrim($currentRoute['action'], '$')] . 'Action';
// here I make the assumption that this parameter is an array. You might want to add a check here
$parameters = array();
foreach ($currentRoute['parameters'] as $parameter) {
$parameters[] = $matches[ltrim($parameter, '$')];
}
// check before instantiating
if (!class_exists($controllerClass)) {
die('invalid controller');
}
$controller = new $controllerClass();
// also check before invoking the method
if (!method_exists($controller, $actionMethod)) {
die('invalid method');
}
// this PHP function allows to call the function with a variable number of parameters
call_user_func_array(array($controller, $actionMethod), $parameters);
One reason why your approach is not very favorable is that you make a lot of assumptions:
the regex needs to have as many groups as you use in the other parameters
if you are imprecise with the regex, it might be possible to call any method in your code
Maybe this will be good enough for your project but you should consider using a well-established router if you want to create something not for educational purposes.
The following code works and does what I want, but I'm pretty sure I'm doing something dumb\awful.
I'm learning OOP and there is a tutorial I started to follow that used a "Config" class to setup some parameters for the program to use. I've noticed something similar in other tutorials. This tutorial though only included a method to retrieve the configuration (it used the $GLOBALS array) not to update it during the run time of the program. I attempted to add this functionality, but resorted to using eval() which I think is a nono? Also it was never explained in the tutorial why the $GLOBALS array was used instead of just using a static variable so I'm confused about that as well.
Here is init.php which gets included in files needing to access the config options:
<?php
$GLOBALS['config'] = array(
'mysql' => array(
'host' => '127.0.0.1',
'username' => 'root',
'password' => '123456',
'db' => NULL
),
'shell' => array(
'exe' => 'powershell.exe',
'args' => array(
'-NonInteractive',
'-NoProfile',
'-NoLogo',
'-Command'
)
)
);
spl_autoload_register(function($class){
require_once 'classes/' . $class . '.php';
});
This is the Config.php class which has a get and (my) set method to access the config array. For the set method I build a string like "$GLOBALS['config']['someConfig']['someSubConfig'] = 'newVal';" and use eval to execute it. Ultimately I use it in the program like Config::set('mysql/host','zzzzz');
<?php
class Config {
public static function get($path=NULL) {
//return all configs if not specified
$config = $GLOBALS['config'];
if($path) {
//parse path to return config
$path = explode('/', $path);
foreach($path as $element) {
if(isset($config[$element])) {
$config = $config[$element];
} else {
//if config not exist
$config = false;
}
}
}
return $config;
}
public static function set($path=NULL,$value=NULL) {
if($path) {
//parse path to return config
$path = explode('/', $path);
//Start code string for eval
$globalPosition = '$GLOBALS['."'config'".']';
foreach($path as $element) {
$globalPosition .= "['$element']";
}
$globalPosition .= "='$value';";
//End code string
eval($globalPosition);
var_dump($GLOBALS);
}
}
}
First of all, here are a few caveats:
Global variables are rarely a good idea, especially in OOP design (mainly because they couple code very tightly).
Please don't use eval().
You can quite easily modify your code to set the variable (by reference using =&) without having to use eval() at all. For example:
public static function set($path = null,$value = null)
{
if($path)
{
//parse path to return config
$path = explode('/', $path);
//Start code string for eval
$setting =& $GLOBALS['config'];
foreach($path as $element)
{
$setting =& $setting[$element];
}
$setting = $value;
var_dump($GLOBALS);
}
}
Normally I use the Zend Framework and this is something I miss in Lithium. Partials. There is a render method in the view where you can use 'elements' which is the closest I got.
<?php $this->_render('element', 'form); ?>
This does work, however it requires that the form.html.php file is in the /views/elements folder. Is it possible to let it search in another path? Like /views/users/ so it gets the file /views/users/form.html.php.
I have tried the following, since I found out that the render method does accept an options argument wherein you can specify a path. So I made an Helper to fix this problem for me.
namespace app\extensions\helper;
use lithium\template\TemplateException;
class Partial extends \lithium\template\Helper
{
public function render($name, $folder = 'elements', $data = array())
{
$path = LITHIUM_APP_PATH . '/views/' . $folder;
$options['paths']['element'] = '{:library}/views/' . $folder . '/{:template}.{:type}.php';
return $this->_context->view()->render(
array('element' => $name),
$data,
$options
);
}
}
However it still only searches in the /view/elements folder, not in the path I specified.
Is there something I am doing wrong?
Why using plugins when this stuff can hopefully be done by Lithium :-)
I don't know Zend, but here is an exemple to configure elements default paths differently, to load them from the related view folder, instead of a shared path.
And let's add one more thing: we want to differentiate elements/partials from a normal view, by appending un underscore to the name of the file (mimic Rails partials)
First, reconfigure Media during the bootstrap process (config/bootstrap/media.php)
Media::type('default', null, array(
'view' => 'lithium\template\View',
'paths' => array(
'layout' => '{:library}/views/layouts/{:layout}.{:type}.php',
'template' => '{:library}/views/{:controller}/{:template}.{:type}.php',
'element' => array(
'{:library}/views/{:controller}/_{:template}.{:type}.php',
'{:library}/views/elements/{:template}.{:type}.php'
)
)
));
Then, use it
Suppose a controller Documents. Call on a view:
<?= $this->_render('element', 'foo', $data, array('controller' => 'documents')); ?>
This will look for a file inside views/documents/_foo.html.php and if doesn't exists, fallback to /views/elements/foo.html.php
This kind of simple re-configuration of framework defaults, can be done in Lithium for a bunch of stuffs (default controllers paths to create namespaces, views paths, libraries, etc ...)
One more example to re-maps your template paths so you can have stuff like pages/users_{username}.php instead of the Lithium default:
https://gist.github.com/1854561
Fixed it. Works like a charm. Zend like Partials in Lithium.
<?php
namespace app\extensions\helper;
use lithium\template\View;
class Partial extends \lithium\template\Helper
{
public function render($name, $folder = 'elements', array $data = array())
{
$view = new View(array(
'paths' => array(
'template' => '{:library}/views/' . $folder . '/' . $name . '.{:type}.php'
)
));
return $view->render('all', $data);
}
}
Can be used in templates like:
<?php echo $this->partial->render('filename', 'foldername', compact('foo', 'bar')); ?>
There is a plugin for partials. https://github.com/dmondark/li3_partials
Ok here is a method I use for initializing models in my controller actions:
protected $_tables = array();
protected function _getTable($table)
{
if (false === array_key_exists($table, $this->_tables)) {
include APPLICATION_PATH . '/modules/'
. $this->_request->getModuleName() . '/models/' . $table . '.php';
$this->_tables[$table] = new $table();
echo 'test ';
}
return $this->_tables[$table];
}
Then when I call the _getTable() method two times (for example once in init() method and once in the controller action) it prints:
test test test test test test
On top of the page. Shouldn't it just return the object from the _tables array() because of the array_key_exists() check? In other words shouldn't the part inside the array_key_exists() function get executed only once when the method is called multiple times?
UPDATE:
So the problem is this - for some reason the layout gets printed twice (so it's layout printed and inside the layout where there is layout()->content; ?> it prints the layout again). I have no idea why it does this as it worked well on the previous server and also on localhost.
In the snippet you show:
protected $this->_tables = array();
This is not valid syntax, it should be:
protected $_tables = array();
Also, why not just use include_once and let PHP handle this for you? Alternatively, you could use the Zend_Loader. Don't reinvent the wheel.
What you are really looking for is the loading of module based resources. Instead of re-inventing the wheel, why not just use the (module) resource autoloaders of ZF? See the documentation at:
http://framework.zend.com/manual/en/zend.loader.autoloader-resource.html
When you use Zend_Application (I'm assuming you don't), you get these automatically. If you don't you could do something like
$loaders = array();
$frontController = Zend_Controller_Front::getInstance();
foreach($frontController->getControllerDirectory() as $module => $directory) {
$resourceLoader = new Zend_Application_Module_Autoloader(array(
'namespace' => ucfirst($module) . '_',
'basePath' => dirname($directory),
));
$resourceLoader->addResourceTypes(array(
'table' => array(
'path' => 'models/',
'namespace' => 'Table'
));
$loaders[$module] = $resourceLoader;
}
//build array of loaders
$loader = Zend_Loader_Autoloader::getInstance();
$loader->setAutoloaders($loaders);
//set them in the autoloader
This approach is a bit naive, but it should give you nice autoloading.