Okay so I have been looking at developing my own custom CMS system for a specific market and I have been looking through a few frameworks and the problem that I have with them is that they don't automate routing. In laravel, if I remember correctly to respond to a url like this you would do something like this:
Route::get('/user', function()
{
return "This is a user";
});
This would essentially listen for a specific request. Now my idea to simplify this was to create an automated router. So what I did was setup an .htaccess file which took every request and directed it to index.php. It also took anything after the .com, so something like www.testsite.com/admin/pages/edit/5 and appended it as a get variable.
So in the example above I am passing four parameters of a single request:
admin - request path / used to signify a login check must be done before passing
them on to their request
pages - This would be the class or object
edit - This would be the method called from the class / object
5 - This would be the actual row of the record in the database being edited
So I developed a router class which looks something like this:
class router {
public $url;
public $protectedPaths = array('admin','users','client');
public function __construct() {
$this -> url = explode('/', $_GET['url']);
if($this -> url[0] == '') {
$this -> loadDefaultView();
} else {
// Check to ensure that the path is not protected
if(in_array($this -> url[0], $this -> protectedPaths)) {
// check to ensure user is logged in
if($_COOKIE['isLogged']) {
// This means that there is no action or model needed just a returned view
if($this -> url[2] == '') {
$this -> loadViewWithoutAction();
} else {
// we check to ensure there is a controller
if(file_exists(baseControllers .'controller.'. $this -> url[1] .'.php')) {
// require that controller and instantiate it
require baseControllers .'controller.'. $this -> url[1] .'.php';
$obj = new $this -> url[1];
// check to see if method exists
if(method_exists($obj, $this -> url[2])) {
if($_POST) {
$data = $_POST;
} else {
$data = array($this -> url[3]);
}
// run method if necessary
$data = call_user_func_array(array($obj, $this -> url[2]), $data);
$this -> loadAdminView( $data );
} else {
$this -> loadErrorView();
}
} else {
$this -> loadErrorView();
}
}
} else {
header("Location: /auth/form");
}
} else {
// we check to ensure there is a controller
if(file_exists(baseControllers .'controller.'. $this -> url[0] .'.php')) {
// require that controller and instantiate it
require baseControllers .'controller.'. $this -> url[0] .'.php';
$obj = new $this -> url[0];
// check to see if method exists
if(method_exists($obj, $this -> url[1])) {
// run method if necessary
$data = call_user_func_array(array($obj, $this -> url[1]), array($this -> url[2]));
$this -> loadPublicView( $data );
} else {
$this -> loadErrorView();
}
} else {
$this -> loadErrorView();
}
}
}
}
So I would use a number of different if else statements and perhaps a switch to differentiate between different requests etc. Finally my question, is this bad practice to auto load a class and run a method. From what I've seen in the frameworks, this is all manual and I'm no expert so I'm assuming there is probably a reason for that. In addition, I found little to anything about automating PHP requests in OOP on the web.
I'd really like to automate this but at the same time I also do not want to cause security concerns. Oh and for any forms or personal information being input by users everything would be post or ajax to protect against hacking the url.
Thanks for any advice or answers in advance!
Related
Before I ask my question I want to make sure a few things are clear:
1) I read the official PHP manual on OOP (intro, basics, inheritance, properties and so on) It seems im honestly not getting something. Im on it for hours now and fixed a few things there and then but new errors are popping up.
2) I read the error messages Missing argument 1 for Main::__construct() and Undefined variable: array. This means my $start variable is NULL.
3) I did do a few searches on stackoverflow either it is not fully related or very hard for me understand whats going on since the example code is so complex (Not the best thing for a starter).
My question: Why is the below code not working? I would really appreciate why it is failing and what I have to consider.
class Main {
protected $config;
protected $start;
function __construct($arr) {
if(!isset($this -> start)) {
if ($arr['secure']){
$this -> config = parse_ini_file('C:\xampp\htdocs\project\config.ini');
$this -> start = mysqli_connect($this -> config['host'], $this -> config['username'],
$this -> config['password'], $this -> config['database']);
}
else {
$this -> start = mysqli_connect($arr['host'], $arr['username'], $arr['password'], $arr['database']);
}
}
if($this -> start == false) {
return mysqli_connect_error();
}
}
}
$Main = new Main($array = ["host" => '', "username" => '', "password" => '', "database" => '',
"errors" => false, "secure" => true]);
class Test extends Main {
public function sample() {
$query = "SELECT id, user,
FROM users";
$result = mysqli_query($Main -> start , $query);
$row = mysqli_fetch_array($result);
echo $row['user'];
}
}
$x = new Test();
$x -> sample();
So here's what happens on run time:
$Main = new Main(...);
OK, you may get a connection there if those details are filled in, but there is an issue in determining whether you made a successful connection or not. See below for more info, but $Main is important to note for now.
$x = new Test();
Class Test extends your class Main.
Your class Test therefore inherits the class Main's constructor, which requires an argument. The argument isn't provided, so it generates a warning.
To account for this, make $arr optional if you're not always going to be passing it:
function __construct($arr = array())
Then check if it exists by using isset on the index:
if(isset($arr['secure'])) // ...
Fast forward, because you haven't provided $arr, you will not be able to successfully connect to your DB. According to mysqli::__construct(), which mysqli_connect is an alias of, it will try to return an object ("which represents the connection to a MySQL Server.").
Now, take a look at these lines:
$this -> start = mysqli_connect( ... )
// ...
if ($this -> start == false) {
You must check against the return's connect_error attribute (or mysqli_connect_error()) to verify if the connection worked or not.
Thanks to the comments below, you should ALSO check for a false assignment, as mysqli_connect has been known to generate a warning and return false too, even though it is not shown on the PHP docs.
Let's continue.
$x -> sample();
Test::sample uses mysqli_query which expects the database connection as it's first argument. You attempt this by passing $Main->start.
Unfortunately, $Main is not in your variable scope, and cannot be accessed from inside of that method. What you need to reference is $this->start.
In fact, if this is the only reason you instantiated $Main, then you don't need to at that point. Instead, pass the connection details array through to new Test() instead.
Here's two solutions:
Pass your DB connection details through to $x = new Test();
The instance will then connect to the DB as intended and will be able to run the query.
Separate class Test from class Main, and pass an instance of class Main through your class Test constructor.
Probably the better option. If your Test is meant to be for your query don't have it extend Main, create your Database connection object (new Main(..)) and then pass that through into new Test($Main). In your Test class, create a constructor which accepts the Main class as an argument:
public function __construct(Main $main)
{
$this->db = $main;
}
Allow access to your connection object by making $main's $start attribute public, or by creating a getter function (e.g. getConnection()), then use than in your query:
$result = mysqli_query($this->db->getConnection(), $query);
There's many, many ways you can approach your scenario, it's down to you.
The answers have addressed the initial problems, but I thought I might also offer a few implementation suggestions also.
You are working with two different instances.
// This is one instance
$Main = new Main([...]);
// This is a different instance.
$test = new Test();
Extending a class does not mean that it gets the values from the existing instances of the class. It only means that it gets the same methods (and default properties).
Therefore, your class Test gets the same constructor as Main, meaning you need to send in the array to Test to be able to use instantiate it. In your example, there is no reason to instantiate Main directly at all.
$result = mysqli_query($Main -> start , $query);
to
$result = mysqli_query($this -> start , $query);
That removes the syntax error at least. $this is an introspective variable, it always refers to the current scope of instances.
Check out the comments below
//I suggest to make this class abstract to make sure
//php doesn't let you to instantiate it directly as you did before
abstract class Main {
protected $config;
protected $start;
function __construct($arr) {
if(!isset($this -> start)) {
if ($arr['secure']){
$this -> config = parse_ini_file('C:\xampp\htdocs\project\config.ini');
$this -> start = mysqli_connect($this -> config['host'], $this -> config['username'],
$this -> config['password'], $this -> config['database']);
}
else {
$this -> start = mysqli_connect($arr['host'], $arr['username'], $arr['password'], $arr['database']);
}
}
if($this -> start == false) {
return mysqli_connect_error();
}
}
}
class Test extends Main {
public function sample() {
$query = "SELECT id, user,
FROM users";
//Here you should use $this instead of $Main as you can access protected
//properties of the parent from the child class.
$result = mysqli_query($this -> start , $query);
$row = mysqli_fetch_array($result);
echo $row['user'];
}
}
//Instantiate Test class instead of Main as it inherits Main anyway
//Your code also had a typo in the constructor argument.
$x = new Test(array("host" => '', "username" => '', "password" => '', "database" => '', "errors" => false, "secure" => true));
$x -> sample();
Please not that I didn't check the mysql part of your code - just an OOP structure
I have component called mycomponent
models
paypal.php
controllers
paypal.php
views
paypal
view.html.php
index.html
tmpl(folder)
default.php
index.html
In controller i have this code
<?php
// No direct access.
defined('_JEXEC') or die;
jimport('joomla.application.component.controlleradmin');
/**
* Objectdefects list controller class.
*/
class MycomponentControllerPaypal extends JControllerAdmin
{
public function paypaldetails()
{
$model = $this->getModel('paypal');
// Get token
$token = urlencode(htmlspecialchars(JRequest::getVar('token')));
if (!$token)
{
// Missing $token parameter
$app = JFactory::getApplication();
$app->enqueueMessage(JText::_('COM_INSTALLER_MSG_MISSING_TOKEN'));
}
else
{
// Install plugin
$model->paypaldetails($token);
}
}
}
In model i have this fragment of code
public function paypaldetails($token){
$environment= $this->environment;
// Add request-specific fields to the request string.
$nvpStr = "&TOKEN=$token";
// Execute the API operation; see the PPHttpPost function above.
$httpParsedResponseAr = $this->PPHttpPost('GetExpressCheckoutDetails', $nvpStr);
//var_dump($httpParsedResponseAr);
if("SUCCESS" == strtoupper($httpParsedResponseAr["ACK"]) || "SUCCESSWITHWARNING" == strtoupper($httpParsedResponseAr["ACK"])) {
$paypaldetails=array();
$paypaldetails["firstname"]= $httpParsedResponseAr['FIRSTNAME'];
$paypaldetails["lastname"] = $httpParsedResponseAr["LASTNAME"];
$paypaldetails["countrycode"] = $httpParsedResponseAr["COUNTRYCODE"];
$this->paypaldetails=$paypaldetails;
$a=$this->paypaldetails;
var_dump($a);
} else {
exit('GetExpressCheckoutDetails failed: ' . print_r($httpParsedResponseAr, true));
}
}
In view/template/default.php i have this
<?php
// no direct access
defined('_JEXEC') or die;
// Import CSS
$document = JFactory::getDocument();
$document->addStyleSheet('components/com_mycomponent/assets/css/defects.css');
$results = $this->items;
var_dump($results);
echo 'Firstname: '.$results[firstname];
echo '<br>Lastname: '.$results[lastname];
echo '<br>Countrycode: '.$results[countrycode];
When i run this url index.php?option=com_fewostar&view=paypal&task=paypal.paypaldetails&token=EC-92L7275685367793U&PayerID=TGWAUKNJLH2WL
I view first var_dump($a); located on model, but second var_dump($results); located in views/paypal/tmpl/default.php not display, and field in view not display. for any reason this url not call view. When i run this url index.php?option=com_fewostar&view=paypal code without task, view is display. but for this url
index.php?option=com_fewostar&view=paypal&task=paypal.paypaldetails&token=EC-92L7275685367793U&PayerID=TGWAUKNJLH2WL no display view. How i call view for this task, may be i need other view file, different of default.php?
I see a couple of problems here.
First, the code is not exactly using Joomla MVC style (even if it works for you, might be harder for people familiar with Joomla to debug).
Model method should be called getPaypaldetails and return something
public function getPaypaldetails()
{
// For Joomla 1.7+ use JInput instead of JRequest (deprecated)
$token = JFactory::getApplication()->input->getVar('token');
// some code
return $paypaldetails;
}
view.html.php should and get and data from model and assign to itself
public function display($tpl = null)
{
// Get some data from the models
$items = $this->model->get('paypaldetails');
// If data are incorrect, show nice error message
// ...
$this->items = $items;
}
View Layout file should be placed in /com_fewostar/views/paypal/tmpl/default.php
By default, the view is only called by the "display" task (which is the default task). Since you use your own task, you need to either redirect to the view after your task is finished or try to load the display function at the end.
I've literally downloaded Laravel today and like the looks of things but i'm struggeling on 2 things.
1) I like the controllers' actions method of analysing urls instead of using routes, it seems to keep everything together more cleanly, but lets say I want to go to
/account/account-year/
how can I write an action function for this? i.e.
function action_account-year()...
is obviously not valid syntax.
2) If i had
function action_account_year( $year, $month ) { ...
and visited
/account/account_year/
An error would be displayed about missing arguments, how do you go about making this user friendly/load diff page/display an error??
You would have to manually route the hyphenated version, e.g.
Route::get('account/account-year', 'account#account_year');
Regarding the parameters, it depends on how you are routing. You must accept the parameters in the route. If you are using full controller routing (e.g. Route::controller('account')) then the method will be passed parameters automatically.
If you are manually routing, you have to capture the params,
Route::get('account/account-year/(:num)/(:num)', 'account#account_year');
So visiting /account/account-year/1/2 would do ->account_year(1, 2)
Hope this helps.
You can think of the following possibility as well
class AccountController extends BaseController {
public function getIndex()
{
//
}
public function getAccountYear()
{
//
}
}
Now simply define a RESTful controller in your routes file in the following manner
Route::controller('account', 'AccountController');
Visiting 'account/account-year' will automatically route to the action getAccountYear
I thought I'd add this as an answer in case anyone else is looking for it:
1)
public function action_account_year($name = false, $place = false ) {
if( ... ) {
return View::make('page.error' );
}
}
2)
not a solid solutions yet:
laravel/routing/controller.php, method "response"
public function response($method, $parameters = array())
{
// The developer may mark the controller as being "RESTful" which
// indicates that the controller actions are prefixed with the
// HTTP verb they respond to rather than the word "action".
$method = preg_replace( "#\-+#", "_", $method );
if ($this->restful)
{
$action = strtolower(Request::method()).'_'.$method;
}
else
{
$action = "action_{$method}";
}
$response = call_user_func_array(array($this, $action), $parameters);
// If the controller has specified a layout view the response
// returned by the controller method will be bound to that
// view and the layout will be considered the response.
if (is_null($response) and ! is_null($this->layout))
{
$response = $this->layout;
}
return $response;
}
I have a problem with CI. I have a model:
public function Game($id) {
$id = (int)$id;
$q = $this -> db -> get_where('games', array('id' => $id));
return $q -> row_array();
}
Controller for it:
public function index($gameID) {
$data['game'] = $this->games_model->Game($gameID);
$this -> load -> view('games/game', $data);
}
And a problem ;) I've set my routing as follows:
$route['games/(:num)'] = 'games/game/$1';
$route['games'] = 'games/game/game';
But it doesn't work at all. My controller dir is games/game.php (with function Game inside). My problem is - how can I pass $id for it? I am very new to CI, but I couldn't find solution for this in docs.
$route['games'] = 'games/game/index'; // Folder/Controller/Function
$route['games/(:num)'] = 'games/game/index/$1'; // Folder/Controller/Function/Method
$route['games/(:num)/(:any)'] = 'games/game/index/$1/$2';
If you want to use slug, url_title($title, 'underscore', TRUE) it help you
You can use either remap or you will have to modify your routes path again. Take a look at this question, that's been asked here.
This situation arises from someone wanting to create their own "pages" in their web site without having to get into creating the corresponding actions.
So say they have a URL like mysite.com/index/books... they want to be able to create mysite.com/index/booksmore or mysite.com/index/pancakes but not have to create any actions in the index controller. They (a non-technical person who can do simple html) basically want to create a simple, static page without having to use an action.
Like there would be some generic action in the index controller that handles requests for a non-existent action. How do you do this or is it even possible?
edit: One problem with using __call is the lack of a view file. The lack of an action becomes moot but now you have to deal with the missing view file. The framework will throw an exception if it cannot find one (though if there were a way to get it to redirect to a 404 on a missing view file __call would be doable.)
Using the magic __call method works fine, all you have to do is check if the view file exists and throw the right exception (or do enything else) if not.
public function __call($methodName, $params)
{
// An action method is called
if ('Action' == substr($methodName, -6)) {
$action = substr($methodName, 0, -6);
// We want to render scripts in the index directory, right?
$script = 'index/' . $action . '.' . $this->viewSuffix;
// Script file does not exist, throw exception that will render /error/error.phtml in 404 context
if (false === $this->view->getScriptPath($script)) {
require_once 'Zend/Controller/Action/Exception.php';
throw new Zend_Controller_Action_Exception(
sprintf('Page "%s" does not exist.', $action), 404);
}
$this->renderScript($script);
}
// no action is called? Let the parent __call handle things.
else {
parent::__call($methodName, $params);
}
}
You have to play with the router
http://framework.zend.com/manual/en/zend.controller.router.html
I think you can specify a wildcard to catch every action on a specific module (the default one to reduce the url) and define an action that will take care of render the view according to the url (or even action called)
new Zend_Controller_Router_Route('index/*',
array('controller' => 'index', 'action' => 'custom', 'module'=>'index')
in you customAction function just retrieve the params and display the right block.
I haven't tried so you might have to hack the code a little bit
If you want to use gabriel1836's _call() method you should be able to disable the layout and view and then render whatever you want.
$this->_helper->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true);
I needed to have existing module/controller/actions working as normal in a Zend Framework app, but then have a catchall route that sent anything unknown to a PageController that could pick user specified urls out of a database table and display the page. I didn't want to have a controller name in front of the user specified urls. I wanted /my/custom/url not /page/my/custom/url to go via the PageController. So none of the above solutions worked for me.
I ended up extending Zend_Controller_Router_Route_Module: using almost all the default behaviour, and just tweaking the controller name a little so if the controller file exists, we route to it as normal. If it does not exist then the url must be a weird custom one, so it gets sent to the PageController with the whole url intact as a parameter.
class UDC_Controller_Router_Route_Catchall extends Zend_Controller_Router_Route_Module
{
private $_catchallController = 'page';
private $_catchallAction = 'index';
private $_paramName = 'name';
//-------------------------------------------------------------------------
/*! \brief takes most of the default behaviour from Zend_Controller_Router_Route_Module
with the following changes:
- if the path includes a valid module, then use it
- if the path includes a valid controller (file_exists) then use that
- otherwise use the catchall
*/
public function match($path, $partial = false)
{
$this->_setRequestKeys();
$values = array();
$params = array();
if (!$partial) {
$path = trim($path, self::URI_DELIMITER);
} else {
$matchedPath = $path;
}
if ($path != '') {
$path = explode(self::URI_DELIMITER, $path);
if ($this->_dispatcher && $this->_dispatcher->isValidModule($path[0])) {
$values[$this->_moduleKey] = array_shift($path);
$this->_moduleValid = true;
}
if (count($path) && !empty($path[0])) {
$module = $this->_moduleValid ? $values[$this->_moduleKey] : $this->_defaults[$this->_moduleKey];
$file = $this->_dispatcher->getControllerDirectory( $module ) . '/' . $this->_dispatcher->formatControllerName( $path[0] ) . '.php';
if (file_exists( $file ))
{
$values[$this->_controllerKey] = array_shift($path);
}
else
{
$values[$this->_controllerKey] = $this->_catchallController;
$values[$this->_actionKey] = $this->_catchallAction;
$params[$this->_paramName] = join( self::URI_DELIMITER, $path );
$path = array();
}
}
if (count($path) && !empty($path[0])) {
$values[$this->_actionKey] = array_shift($path);
}
if ($numSegs = count($path)) {
for ($i = 0; $i < $numSegs; $i = $i + 2) {
$key = urldecode($path[$i]);
$val = isset($path[$i + 1]) ? urldecode($path[$i + 1]) : null;
$params[$key] = (isset($params[$key]) ? (array_merge((array) $params[$key], array($val))): $val);
}
}
}
if ($partial) {
$this->setMatchedPath($matchedPath);
}
$this->_values = $values + $params;
return $this->_values + $this->_defaults;
}
}
So my MemberController will work fine as /member/login, /member/preferences etc, and other controllers can be added at will. The ErrorController is still needed: it catches invalid actions on existing controllers.
I implemented a catch-all by overriding the dispatch method and handling the exception that is thrown when the action is not found:
public function dispatch($action)
{
try {
parent::dispatch($action);
}
catch (Zend_Controller_Action_Exception $e) {
$uristub = $this->getRequest()->getActionName();
$this->getRequest()->setActionName('index');
$this->getRequest()->setParam('uristub', $uristub);
parent::dispatch('indexAction');
}
}
You could use the magic __call() function. For example:
public function __call($name, $arguments)
{
// Render Simple HTML View
}
stunti's suggestion was the way I went with this. My particular solution is as follows (this uses indexAction() of whichever controller you specify. In my case every action was using indexAction and pulling content from a database based on the url):
Get an instance of the router (everything is in your bootstrap file, btw):
$router = $frontController->getRouter();
Create the custom route:
$router->addRoute('controllername', new Zend_Controller_Router_Route('controllername/*', array('controller'=>'controllername')));
Pass the new route to the front controller:
$frontController->setRouter($router);
I did not go with gabriel's __call method (which does work for missing methods as long as you don't need a view file) because that still throws an error about the missing corresponding view file.
For future reference, building on gabriel1836 & ejunker's thoughts, I dug up an option that gets more to the point (and upholds the MVC paradigm). Besides, it makes more sense to read "use specialized view" than "don't use any view".
// 1. Catch & process overloaded actions.
public function __call($name, $arguments)
{
// 2. Provide an appropriate renderer.
$this->_helper->viewRenderer->setRender('overload');
// 3. Bonus: give your view script a clue about what "action" was requested.
$this->view->action = $this->getFrontController()->getRequest()->getActionName();
}
#Steve as above - your solution sounds ideal for me but I am unsure how you implmeented it in the bootstrap?