I'm creating an mvc structure for learning/teaching purpouses and so far I could set up the structure and a controller plus twig as template system.
The structure is:
index.php
controllers/
error.php
inc/
controller_base.php
view_manager.php
views/
.cache/
error/
view.html
So:
index instantiate twig autoloader (and mvc autoloader by spl_register).
index instantiate error controller inheriting controller_base.
controller_base is holding the view_manager.
error call view_manager to display the error/view.html and the only thing I get on the browser is error/view.html.
No errors on the apache log. (error_reporting(E_ALL))
Twig cache files created correctly, but the content doesn't look good to me:
protected function doDisplay(array $context, array $blocks = array()) {
// line 1
echo "error/view.html";
}
Anyone knows why, and how can I print the actual view?
Thanks in advance.
Code:
index.php: Declaring autoloaders
function __autoload($class_name)
{
if(file_exists("controllers/$class_name.php")):
include strtolower("controllers/$class_name.php");
elseif(file_exists("models/$class_name.php")):
include strtolower("models/$class_name.php");
elseif(file_exists("inc/$class_name.php")):
include strtolower("inc/$class_name.php");
endif;
}
spl_autoload_register('__autoload');
require_once 'vendor/autoload.php';
Twig_Autoloader::register(); has been avoided cause Twig installation was done by composer.
Adding it doesn't bring any change.
error.php (controller): called method.
public function show($param)
{
$this->viewMng->display(get_class().$data['view'], array())
}
controller_base.php:
class base
{
protected $viewMng;
public function __construct()
{
$this->viewMng = new viewmanager();
}
}
viewmanager.php: whole class
class viewmanager {
private $twig;
protected $template_dir = 'views/';
protected $cache_dir = 'views/.cache';
// protected $vars = array();
public function __construct($template_dir = null) {
if ($template_dir !== null) {
// Check here whether this directory really exists
$this->template_dir = $template_dir;
}
$loader = new Twig_Loader_String($this->template_dir);
$this->twig = new Twig_Environment($loader, array(
'cache' => $this->cache_dir));
}
public function render($template_file, $data = array()) {
if (!file_exists($this->template_dir.$template_file)) {
throw new Exception('no template file ' . $template_file . ' present in directory ' . $this->template_dir);
}
return $this->twig->render($template_file, $data);
}
public function display($template_file, $data) {
if (!file_exists($this->template_dir.$template_file)) {
throw new Exception('no template file ' . $template_file . ' present in directory ' . $this->template_dir);
}
$tmpl = ($this->twig->loadTemplate($template_file));//print_r($tmpl);
$tmpl->display($data);
}
}
view.html:
<html><body> Hello </body></html>
The problem is based on the loader.
According to the Twig documentation:
Twig_Loader_String loads templates from strings. It's a dummy loader
as the template reference is the template source code This
loader should only be used for unit testing as it has severe
limitations: several tags, like extends or include do not make sense
to use as the reference to the template is the template source code
itself.
That's why it only prints the passed in string.
Twig_Loader_String, should be replaced by the proper loader.
In this case it works perfectly well Twig_Loader_Filesystem.
$loader = new Twig_Loader_Filesystem($this->template_dir);
This solve the problem and the MVC structure works completely fine.
Thanks taking a look, guys.
Related
I want to upgrade my Twig from very old version (2.x or even 1.x) to 3.3. In old version macros imported in top level template are available in all extened and included templates. And I have a bunch of templates where I need my macros (100+ and many embed blocks). I don't want to import macros manually in evety template.
So, according to similar question Twig auto import macros by environment I tried to implement suggested solution, but it doesn't work.
Actually I tried this:
$tpl = $this->Twig->load('inc/function.twig');
$this->Twig->addGlobal('fnc', $tpl);
I also tried this:
$tpl = $this->Twig->load('inc/function.twig');
$this->Twig->addGlobal('fnc', $tpl->unwrap());
but I have same result. In templates fnc is defined but it's a Template object and I can not access macros. I get a fatal error when I try to do it:
Fatal error: Uncaught Twig\Error\RuntimeError: Accessing \Twig\Template attributes is forbidden
As I understand, in Twig 3 you can not just include macros using addGlobal.
Old Twig was added to our repository (was not ignored) and we probably will add to repository new Twig too, so it's possible to modify Twig's source code.
UPD:
When I try just to addGlobal my template with macros I get
Fatal error: Uncaught LogicException: Unable to add global "fnc" as the runtime or the extensions have already been initialized.
I've solved this problem using this solution (I've extended Environment class).
During some testing I found out you can still call the "functions" defined inside a macro with pure PHP
<?php
$wrapper = $twig->load('macro.html');
$template = $wrapper->unwrap();
echo $template->macro_Foo().''; //return a \Twig\Markup
With this in place you could write a wrapper around the macro and try to auto load them in a container.
First off we need an extension to enable and access the container
<?php
class MacroWrapperExtension extends \Twig\Extension\AbstractExtension
{
public function getFunctions()
{
return [
new \Twig\TwigFunction('macro', [$this, 'macro'], ['needs_environment' => true,]),
];
}
protected $container = null;
public function macro(\Twig\Environment $twig, $template) {
return $this->getContainer($twig)->get($template);
}
private function getContainer(\Twig\Environment $twig) {
if ($this->container === null) $this->container = new MacroWrapperContainer($twig);
return $this->container;
}
}
Next the container itself. The container is responsible to load and store/save the (auto loaded) macros in the memory. The code will try to locate and load any file in the map macros in your view folder.
template
|--- index.html
|--- macros
|------- test.html
|
<?php
class MacroWrapperContainer {
const FOLDER = 'macros';
protected $twig = null;
protected $macros = [];
public function __construct(\Twig\Environment $twig) {
$this->setTwig($twig)
->load();
}
public function get($macro) {
return $this->macros[$macro] ?? null;
}
protected function load() {
foreach($this->getTwig()->getLoader()->getPaths() as $path) {
if (!is_dir($path.'/'.self::FOLDER)) continue;
$this->loadMacros($path.'/'.self::FOLDER);
}
}
protected function loadMacros($path) {
$files = scandir($path);
foreach($files as $file) if ($this->isTemplate($file)) $this->loadMacro($file);
}
protected function loadMacro($file) {
$name = pathinfo($file, PATHINFO_FILENAME);
if (!isset($this->macros[$name])) $this->macros[$name] = new MacroWrapper($this->getTwig()->load(self::FOLDER.'/'.$file));
}
protected function isTemplate($file) {
return in_array(pathinfo($file, PATHINFO_EXTENSION), [ 'html', 'twig', ]);
}
protected function setTwig(\Twig\Environment $twig) {
$this->twig = $twig;
return $this;
}
protected function getTwig() {
return $this->twig;
}
public function __call($method_name, $args) {
return $this->get($method_name);
}
}
Last off we need to mimic the behavior I've posted in the beginning of the question. So lets create a wrapper around the macro template which will be responsible to call the actual functions inside the macro.
As seen the functions inside a macro get prefixed with macro_, so just let auto-prefix every call made to the macro wrapper with macro_
<?php
class MacroWrapper {
protected $template = null;
public function __construct(\Twig\TemplateWrapper $template_wrapper) {
$this->template = $template_wrapper->unwrap();
}
public function __call($method_name, $args){
return $this->template->{'macro_'.$method_name}(...$args);
}
}
Now inject the extension into twig
$twig->addExtension(new MacroWrapperExtension());
This will enable the function macro inside every template, which lets us access any macro file inside the macros folder
{{ macro('test').hello('foo') }}
{{ macro('test').bar('foo', 'bar', 'foobar') }}
I am currently learning MVC pattern and I have a problem with understanding how to pass data from controller to model and then from model to view.
My structure looks like this
/controllers
index.php
/libs
controller.php
main.php
view.php
/models
index_model.php
/views
index.php
.htaccess
index.php
First I always create an main object and include files that i need
include 'libs/Main.php';
require 'libs/Controller.php';
require 'libs/view.php';
$main = new Main();
then in main class i reroute the url so correct controller is called
class Main {
public function __construct() {
$url = (isset($_GET['url']) ? $_GET['url'] : null);
$url = rtrim($url, '/');
$url = explode('/', $url);
if(empty($url[0])){
require'controllers/index.php';
$controller = new Index();
}else{
$file = 'controllers/'. $url[0] .'.php';
if(file_exists($file)){
require $file;
} else
die("Error");
}
}
}
This is the controller for index page
class Index extends Controller{
public function __construct() {
parent::__construct(); //Creates new view object from parents class
$this->model = parent::loadModel('index');
//there should probably exists some method that will pass the data to model
$this->view->render('index'); //renders view, but also it should have another argument that contains data, i think
if(isset($_POST['SubmitCosmonautForm'])){
var_dump($_POST);
}
}
}
This is main controller that every other controller will inherit, so all have created view object
class Controller {
public function __construct(){
$this->view = new view();
}
protected function loadModel($name){
$path = 'models/'.$name.'_model.php';
if(file_exists($path)){
require $path;
$modelName = $name.'_model';
$this->model = new modelName();
}else{
echo "Model for" .$name."doesn't exists";
}
}
}
and this is the view in library, which contains render function so far
class View{
public function render($name){
require 'views/' .$name. '.php';
}
}
For example i want to save form data to database, so when i am thinking about it i should probably pass only data to model, and inside that model i should call methods that will handle saving/loading/changing DB, these methods that operates over DB could be in database class. Is this the right approach ? i read about mvc last 2 hours but i don't know if i understand it correctly.
My question is how to dynamically include and call the model and view classes in a simple MVC model? I have no problem calling the controller, I have found various ways to do that, but I can't find a good solution for calling and passing in the model and view.
I have setup a .htaccess file to read the url as "www.domain.com/controller/method/id".
I was previously trying to do a check if a file exists for the model and view the same way I am doing the controller using the $cont variable, and then trying to load the model and pass it into the controller, then the view. The issue I had is that all the includes are using the $cont variable to call instantiate their classes and could not tell each other apart. I tried adding a suffic $cont . 'Controller', but then I couldn't load the class at all, let alone pass in the model or view.
Here is my latest example without model or view.
<?php
//===============================================
// Debug
//===============================================
ini_set('display_errors','On');
error_reporting(E_ALL);
//===============================================
// Includes
//===============================================
require('coremvc.php');
//===============================================
// Constants & Globals
//===============================================
define('BASE_PATH', dirname(realpath(__FILE__)));
$GLOBALS['var'] = "";
//===============================================
// Session
//===============================================
session_start();
//===============================================
// Router
//===============================================
if ($_SERVER['REQUEST_URI'] !== '/') {
$uri = $_SERVER['REQUEST_URI'];
$uri = ltrim($uri, '/');
$request = explode('/', $uri);
foreach ($request as $key => $val) {
if(empty($request[$key])) {
unset($request[$key]);
}
}
$request = array_values($request);
if (isset($request[0])) {
$cont = $request[0];
}
if (isset($request[1])) {
$action = $request[1];
}
} else {
$cont = "home";
}
if (FILE_EXISTS('/controllers/' . $cont . 'Controller.php')) {
require '/controllers/' . $cont . 'Controller.php';
} else {
$cont = "home";
require '/controllers/homeController.php';
}
//===============================================
// Start the controller
//===============================================
$controller = new $cont;
I have made the following changes to the example above, posted it below, as well as my super easy bake oven simple controller.
<?php
if (FILE_EXISTS('/controllers/' . $cont . 'Controller.php')) {
require '/controllers/' . $cont . 'Controller.php';
} else {
$cont = "home";
$cont = ucfirst($cont) . 'Controller';
require '/controllers/homeController.php';
}
//===============================================
// Start the controller
//===============================================
$controller = new $cont('World');
$controller->world();
Controller, it is just extending an empty class which I was thinking I could use if I wanted to extend a common method to every class. That is what the coremvc.php is in the index.php above.
<?php
Class HomeController extends Controller
{
function __construct($world) {
echo "Hello ";
$this->world = $world;
}
function world() {
echo $this->world;
}
}
You want to load and call classes easily. I dynamically load classes that end in ".class.php". This makes things easy for me.
I put this in my index.php... where /app/ is where I have my php classes:
<?php
define('CLASS_DIR', '/app/');
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . CLASS_DIR);
spl_autoload_extensions('.class.php');
spl_autoload_register();
Next, I require my routes:
require 'rte/myroute.php';
I let my routes (controllers) direct my traffic to my models, albeit some parsing, etc.
I typically develop REST based APIs in PHP, so the "view" is just a JSON response.
The HTML / JavaScript client consumes the responses.
A good framework that I like is SlimPHP. I load it up using Composer.
http://www.slimframework.com/
https://getcomposer.org/
Here's an example of calling a class as an instance and statically, since you auto-loaded, you don't need to include anything at the top:
<?php
$param1 = 1;
$param2 = 2;
MyClass::myFunc($param1);
$myObj = new MyClass;
$myObj->myFunc2($param2);
As it is already say you can have a look to use the spl_autoload_register which will require all your files in the given path. You can change this function to improve the load.
Concerning the Model with your current code you can implement it as follow:
$controllerPath = "/controllers/{$cont}Controller.php";
$modelPath = "/model/{$cont}Model.php";
if (FILE_EXISTS($controllerPath)) {
require $controllerPath;
if (FILE_EXISTS($modelPath)) {
require $modelPath;
}
else {
throw new \LogicException(
sprintf("Your controller must implement a model. No model found: %s", $modelPath)
);
}
} else {
$cont = "home";
require "/controllers/{$cont}Controller.php";
require "/model/{$cont}Model.php";
}
//===============================================
// Start the controller
//===============================================
$controller = new $cont($cont);
In this sample of code, $cont is the name of the page like home. Which require the homeController and homeModel. Then in your __construct($modelName) just set the model.
However, I don't recommand you tu use this because your controller can load many Models. Then, your controller could look like this:
<?php
namespace \App\Controller; // If you use namespace
use App\Model\homeModel, // If you use namespace
App\Model\productModel; // If you use namespace
Class HomeController extends Controller
{
private $model;
/* If this function is common to all controllers just move it
in the parent Controller class( one more time, I don't recommend to set
the model using this way). */
public function __construct($model) {
$this->model= $model;
}
public function login() {
$homeModel = new homeModel(); // If you use namespace
// Example to call the view
$myValue = 'Hello world';
$this->render(array("myVariableName" => $myValue))
}
}
In this second example $this->render can be a method in your Controller class (which by the way should be an abstract class). I'll give a last sample of code for the logic of the abstract controller.
<?php
namespace \App\Controller;
abstract class AbstractController {
/* Common code for all controllers */
public function __construct() {
}
/* View renderer */
protected function render($parameters) {
/* Call the view here maybe an another class which manage the View*/
}
}
To conclude, you can implement this MVC in many way, this code is just a suggestion and maybe its not the best way. I advise you to have a look with the spl_autoload that I put at the beginning of my answer.
I'm trying to build a CMS using the MVC pattern. The structure of my CMS is as follows:
->index.php: entry point. Contains the appropriate includes and creates the frontController which takes a router as it's parameter:
$frontController = new FrontController(new Router);
echo $frontController->output();
->router.php: analyses the URL and defines what the names of each Model,View and Controller will be. By default they are preceded by home, but if the url is of the form http://localhost/index.php?route=Register the MVC classes will be named RegisterModel, RegisterView and Register Controller.
if(isset ( $_GET ['route'] ))
{
$this->URL = explode ( "/", $_GET ['route'] );
$this->route = $_GET ['route'];
$this->model = ucfirst($this->URL [0] . "Model");
$this->view = ucfirst($this->URL [0] . "View");
$this->controller = ucfirst($this->URL [0] . "Controller");
}
else
{
$this->model = "HomeModel";
$this->view = "HomeView";
$this->controller = "HomeController";
$this->route = "Home";
}
->frontController.php: This is where I am stuck. When I go to the homepage, it can be visualised correctly because I already have the default HomeModel,HomeView and HomeController classes created. But I created a link that points to register (localhost/index.php?route=Register) but the PHP log indicates that the appropriate Register classes weren't created by the frontController class.
class FrontController
{
private $controller;
private $view;
public function __construct(Router $router)
{
$modelName = $router->model;
$controllerName = $router->controller;
$viewName = $router->view;
$model = new $modelName ();
$this->controller = new $controllerName ( $model );
$this->view = new $viewName ( $router->getRoute(), $model );
if (! empty ( $_GET['action'] ))
$this->controller->{$_GET['action']} ();
}
public function output()
{
// This allows for some consistent layout generation code
return $this->view->output ();
}
}
At this moment I have no idea how to go about solving this issue. And even if I get the classes to be created in the frontController, is there a way to specify that the classes being dynamically generated should extend from a base Model,View,Controller class?
The default HomeView.php looks like this:
class HomeView
{
private $model;
private $route;
private $view_file;
public function __construct($route, HomeModel $model)
{
$this->view_file = "View/" . $route . "/template.php";
echo $this->view_file;
$this->route = $route;
$this->model = $model;
}
public function output()
{
require($this->view_file);
}
}
Any indications on anything that might help me get unstuck or a pointer in the right direction would be much appreciated.
EDIT1:
I forgot to add a summary of my two issues:
1. I would like to understand why the classes aren't being created in the FrontController class...
2. Once created how would I access those classes? Answer is in the comment section. Using the PHP spl_autoload_register function.
Thanks All!
I am looking for feedback on my control architecture script (included below). Specifically, I am looking for feedback regarding the script's design, organization, commenting, and formatting. I enjoy php programming as a hobby, and am looking to learn where I can improve my code.
Thanks in advance!
class FrontController extends ActionController {
//Declaring variable(s)
private static $instance;
protected $controller;
//Class construct method
public function __construct() {}
//Starts new instance of this class with a singleton pattern
public static function getInstance() {
if(!self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function dispatch($throwExceptions = false) {
/* Checks for the GET variables $module and $action, and, if present,
* strips them down with a regular expression function with a white
* list of allowed characters, removing anything that is not a letter,
* number, underscore or hyphen.
*/
$regex = '/[^-_A-z0-9]+/';
$module = isset($_GET['module']) ? preg_replace($regex, '', $_GET['module']) : 'home';
$action = isset($_GET['action']) ? preg_replace($regex, '', $_GET['action']) : 'frontpage';
/* Generates Actions class filename (example: HomeActions) and path to
* that class (example: home/HomeActions.php), checks if $file is a
* valid file, and then, if so, requires that file.
*/
$class = ucfirst($module) . 'Actions';
$file = $this->pageDir . '/' . $module . '/' . $class . '.php';
try {
//Checks for existance of file
if (!is_file($file)) {
throw new Exception('File not found!');
}
//Includes file
require_once $file;
/* Creates a new instance of the Actions class (example: $controller
* = new HomeActions();), and passes the registry variable to the
* ActionController class.
*/
$controller = new $class();
$controller->setRegistry($this->registry);
//Trys the setModule method in the ActionController class
$controller->setModule($module);
/* The ActionController dispatchAction method checks if the method
* exists, then runs the displayView function in the
* ActionController class.
*/
$controller->dispatchAction($action);
} catch(Exception $error) {
/* An exception has occurred, and will be displayed if
* $throwExceptions is set to true.
*/
if($throwExceptions) {
echo $error;
}
}
}
}
abstract class ActionController {
//Declaring variable(s)
protected $registry;
protected $module;
protected $registryItems = array();
//Class construct method
public function __construct(){}
public function setRegistry($registry) {
//Sets the registry object
$this->registry = $registry;
/* Once the registry is loaded, the controller root directory path is
* set from the registry. This path is needed for the controller
* classes to work properly.
*/
$this->setPageDir();
}
//Sets the controller root directory from the value stored in the registry
public function setPageDir() {
$this->pageDir = $this->registry->get('pageDir');
}
//Sets the module
public function setModule($module) {
$this->module = $module;
}
//Gets the module
public function getModule() {
return $this->module;
}
/* Checks for actionMethod in the Actions class (example: doFrontpage()
* within home/HomeActions.php) with the method_exists function and, if
* present, the actionMethod and displayView functions are executed.
*/
public function dispatchAction($action) {
$actionMethod = 'do' . ucfirst($action);
if (!method_exists($this, $actionMethod)) {
throw new Exception('Action not found!');
}
$this->$actionMethod();
$this->displayView($action);
}
public function displayView($action) {
if (!is_file($this->pageDir . '/' . $this->getModule() . '/' . $action . 'View.php')) {
throw new Exception('View not found!');
}
//Sets $this->actionView to the path of the action View file
$this->actionView = $this->pageDir . '/' . $this->getModule() . '/' . $action . 'View.php';
//Sets path of the action View file into the registry
$this->registry->set('actionView', $this->actionView);
//Includes template file within which the action View file is included
require_once $this->pageDir . '/default.tpl';
}
}
class Registry {
//Declaring variables
private $store;
//Class constructor
public function __construct() {}
//Sets registry variable
public function set($label, $object) {
$this->store[$label] = $object;
}
//Gets registry variable
public function get($label) {
if(isset($this->store[$label])) {
return $this->store[$label];
} else {
return false;
}
}
//Adds outside array of registry values to $this->store array
public function addRegistryArray($registryItems) {
foreach ($registryItems as $key => $value) {
$this->set($key, $value);
}
}
//Returns registry array
public function getRegistryArray() {
return $this->store;
}
}
Without having a detailed look on your code:
Try to write code that is self-explanatory by using meaningful function and variable names. Only use comments where the purpose or the functioning of your code is not clear. E.g.
//Declaring variable(s)
//Class construct method
//Checks for existance of file
//Includes file
are useless comments because the code itself is already clear enough.
A book worth reading: Clean Code
I am torn between closevoting as too localized and wanting to comment on the code. Also, it's too much code to wade through now, so I will comment only a few things:
1) Documentation style
Why not use an established documentation format, like PHPDoc?
2) Formatting
is consistent as far as I can see, but I suggest to use a coding convention that is in wide use, like that of PEAR or ZF (which is based on PEAR) instead of doing your own (yours is close to PEAR anyway, so you might as well adopt it completely).
3) Singleton pattern
In order for the Singleton to work it has to have a private __contruct and __clone method. But I suggest not to use it at all. Many people believe the Singleton is an AntiPattern. There are a number of questions on SO discussing the disadvantages of the Singleton pattern, so have a look around. If there should be only one instance, then simply dont instantiate a second one. If you need to access the FrontController in other classes, inject it.
4) Method length
I probably would try to shorten the dispatch method. Basically, if you describe what a method does and you have to use an and to do so, that part should go into it's own method. Try to make methods into small discrete units. That will make UnitTesting easier as well.
5) Separation of Concerns
I am not sure why the FrontController extends from ActionController. There is no other classes that extent it, butI suppose the classes the FrontController instantiates are subclasses of ActionController too. But the FrontController, while named a controller, does different things than the PageControllers, so I'd probably keep them separated.
On a sidenote, if you are interested in increasing the quality of your code, have a look at the slides and tools given at http://phpqatools.org/
Your code is very remeiscent of CakePhp. I'd recommend checking that out, it does all of this work with App::import() and a File class that wraps the filesystem. It's tested by a community on different versions and OSes, it will in fact save you time.