diff --git a/.travis.yml b/.travis.yml index e11281f..e5debf3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,8 @@ services: - memcached before_script: + - sudo apt-get update > /dev/null + - sudo apt-get install -y --force-yes apache2 libapache2-mod-php5 - phpenv config-add custom.ini - composer self-update - composer install --prefer-source --no-interaction --dev diff --git a/src/Slick/Common/Inspector/Annotation.php b/src/Slick/Common/Inspector/Annotation.php index b18f0ea..f60c130 100644 --- a/src/Slick/Common/Inspector/Annotation.php +++ b/src/Slick/Common/Inspector/Annotation.php @@ -12,6 +12,8 @@ namespace Slick\Common\Inspector; +use Slick\Utility\ArrayMethods; + /** * Annotation * @@ -110,6 +112,19 @@ public function getParameters() return $this->_parameters; } + /** + * Returns the values as an array + * + * @return array + */ + public function allValues() + { + $raw = $this->_parameters['_raw']; + $values = explode(',', $raw); + $result = ArrayMethods::trim($values); + return $result; + } + /** * Fix the parameters for string tags */ diff --git a/src/Slick/Form/AbstractElement.php b/src/Slick/Form/AbstractElement.php index 081c7df..fa107e8 100644 --- a/src/Slick/Form/AbstractElement.php +++ b/src/Slick/Form/AbstractElement.php @@ -278,6 +278,10 @@ public function getHtmlAttributes() $parts = []; foreach ($this->_attributes as $key => $value) { + if (is_numeric($key)) { + $parts[] = $value; + continue; + } $parts[] = "{$key} = \"{$value}\""; } return implode(' ', $parts); diff --git a/src/Slick/Form/Element/SelectMultiple.php b/src/Slick/Form/Element/SelectMultiple.php new file mode 100644 index 0000000..102f511 --- /dev/null +++ b/src/Slick/Form/Element/SelectMultiple.php @@ -0,0 +1,45 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.1.0 + */ + +namespace Slick\Form\Element; +use Slick\Form\Template\AbstractTemplate; + +/** + * Select multiple + * + * @package Slick\Form\Element + * @author Filipe Silva + */ +class SelectMultiple extends Select +{ + + /** + * @readwrite + * @var array HTML attributes + */ + protected $_attributes = [ + 'multiple' + ]; + + /** + * lazy loads a default template for this element + * + * @return AbstractTemplate + */ + public function getTemplate() + { + if (is_null($this->_template)) { + $this->_template = new \Slick\Form\Template\SelectMultiple(); + } + return $this->_template; + } +} diff --git a/src/Slick/Form/Factory.php b/src/Slick/Form/Factory.php index 7acf07d..84309dd 100644 --- a/src/Slick/Form/Factory.php +++ b/src/Slick/Form/Factory.php @@ -37,6 +37,7 @@ class Factory extends Base 'select' => 'Slick\Form\Element\Select', 'area' => 'Slick\Form\Element\Area', 'checkbox' => 'Slick\Form\Element\Checkbox', + 'selectMultiple' => 'Slick\Form\Element\SelectMultiple', ]; /** diff --git a/src/Slick/Form/Template/SelectMultiple.php b/src/Slick/Form/Template/SelectMultiple.php new file mode 100644 index 0000000..30238f8 --- /dev/null +++ b/src/Slick/Form/Template/SelectMultiple.php @@ -0,0 +1,29 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.1.0 + */ + +namespace Slick\Form\Template; + +/** + * SelectMultiple input + * + * @package Slick\Form\Template + * @author Filipe Silva + */ +class SelectMultiple extends AbstractTemplate +{ + + /** + * @readwrite + * @var string + */ + protected $_templateFile = 'selectMultiple-input.html.twig'; +} diff --git a/src/Slick/Form/Template/Views/selectMultiple-input.html.twig b/src/Slick/Form/Template/Views/selectMultiple-input.html.twig new file mode 100644 index 0000000..2e1000f --- /dev/null +++ b/src/Slick/Form/Template/Views/selectMultiple-input.html.twig @@ -0,0 +1,13 @@ +{% extends "default-input.html.twig" %} + +{% block inputElement %} + +{% endblock %} \ No newline at end of file diff --git a/src/Slick/Log/Handler/NullHandler.php b/src/Slick/Log/Handler/NullHandler.php index 0b11b88..282e1b0 100644 --- a/src/Slick/Log/Handler/NullHandler.php +++ b/src/Slick/Log/Handler/NullHandler.php @@ -30,6 +30,6 @@ class NullHandler extends BaseHandler */ public function __construct($level = Logger::DEBUG) { - parent::__construct($level, true); + parent::__construct($level, false); } } diff --git a/src/Slick/Log/Log.php b/src/Slick/Log/Log.php index 8607a3f..71dcb07 100644 --- a/src/Slick/Log/Log.php +++ b/src/Slick/Log/Log.php @@ -33,7 +33,7 @@ class Log extends Base * @read * @var array A list of available loggers. */ - protected $_loggers = array(); + protected static $_loggers = array(); /** * @read @@ -49,9 +49,9 @@ class Log extends Base /** * Gets the logger for the channel with the provided name. - * + * * @param string $name The loggers channel name to retrieve. - * + * * @return \Monolog\Logger The logger object for the given channel name. */ public static function logger($name = null) @@ -62,27 +62,27 @@ public static function logger($name = null) /** * Gets the logger for the channel with the provided name. - * + * * @param string $name The loggers channel name to retrieve. - * + * * @return \Monolog\Logger The logger object for the given channel name. */ public function getLogger($name = null) { $name = is_null($name) ? $this->defaultLogger : $name; - if (!isset($this->_loggers[$name])) { - $this->_loggers[$name] = new Logger($name); - $this->_setDefaultHandlers($this->_loggers[$name]); + if (!isset(static::$_loggers[$name])) { + static::$_loggers[$name] = new Logger($name); + $this->_setDefaultHandlers(static::$_loggers[$name]); } - return $this->_loggers[$name]; + return static::$_loggers[$name]; } /** * Adds the default log handlers to the provided logger. - * + * * @param Logger $logger The logger object to add the handlers. */ - protected function _setDefaultHandlers(Logger $logger) + protected function _setDefaultHandlers(Logger &$logger) { if (empty($this->_handlers)) { $socketHandler = new NullHandler(); diff --git a/src/Slick/Mvc/Application.php b/src/Slick/Mvc/Application.php index 3c09dcd..34a442d 100644 --- a/src/Slick/Mvc/Application.php +++ b/src/Slick/Mvc/Application.php @@ -53,7 +53,7 @@ * request dispatcher * @method Application setLogger(LoggerInterface $logger) Sets a PSR-3 logger */ -final class Application extends Base +class Application extends Base { /** * @readwrite diff --git a/src/Slick/Mvc/Command/GenerateController.php b/src/Slick/Mvc/Command/GenerateController.php new file mode 100644 index 0000000..48bf8a6 --- /dev/null +++ b/src/Slick/Mvc/Command/GenerateController.php @@ -0,0 +1,127 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.0.0 + */ + +namespace Slick\Mvc\Command; + +use Slick\Mvc\Command\Task\GenerateScaffoldController; +use Slick\Mvc\Command\Task\GenerateController as GenerateControllerTask; +use Slick\Mvc\Command\Utils\ControllerData; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Generate controller command + * + * @package Slick\Mvc\Command + * @author Filipe Silva + */ +class GenerateController extends Command +{ + + /** + * Configures the current command. + */ + protected function configure() + { + $this + ->setName("generate:controller") + ->setDescription("Generate a controller file for the provided model name.") + ->addArgument( + 'modelName', + InputArgument::REQUIRED, + 'Full qualified model class name' + ) + ->addOption( + 'path', + 'p', + InputOption::VALUE_OPTIONAL, + 'Sets the application path where controllers are located', + getcwd() + ) + ->addOption( + 'out', + 'o', + InputOption::VALUE_OPTIONAL, + 'The controllers folder where to save the controller.', + 'Controllers' + ) + ->addOption( + 'scaffold', + 'S', + InputOption::VALUE_NONE, + 'If set the controller will have only the scaffold property set.' + ); + } + + /** + * Executes the current command. + * + * This method is not abstract because you can use this class + * as a concrete class. In this case, instead of defining the + * execute() method, you set the code to execute by passing + * a Closure to the setCode() method. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + * + * @return null|integer null or 0 if everything went fine, or an error code + * + * @throws \LogicException When this abstract method is not implemented + * @see setCode() + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + + /** @var Application $application */ + $application = $this->getApplication(); + $output->writeln($application->getLongVersion()); + $output->writeln( + "Generate controller for model ". $input->getArgument('modelName') + ); + $output->writeln(""); + + $controllerData = new ControllerData( + [ + 'controllerName' => $input->getArgument('modelName'), + 'namespace' => $input->getOption('out'), + 'modelName' => $input->getArgument('modelName') + ] + ); + + $path = $input->getOption('path'); + $path .= '/'. $input->getOption('out'); + + if ($input->getOption('scaffold')) { + $task = new GenerateScaffoldController( + [ + 'command' => $this, + 'controllerData' => $controllerData, + 'path' => $path + ] + ); + $task->run($input, $output); + } else { + $task = new GenerateControllerTask( + [ + 'command' => $this, + 'controllerData' => $controllerData, + 'path' => $path + ] + ); + $task->run($input, $output); + } + } +} \ No newline at end of file diff --git a/src/Slick/Mvc/Command/GenerateViews.php b/src/Slick/Mvc/Command/GenerateViews.php new file mode 100644 index 0000000..80fa9c7 --- /dev/null +++ b/src/Slick/Mvc/Command/GenerateViews.php @@ -0,0 +1,94 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.0.0 + */ + +namespace Slick\Mvc\Command; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Generate views command + * + * @package Slick\Mvc\Command + * @author Filipe Silva + */ +class GenerateViews extends Command +{ + + /** + * Configures the current command. + */ + protected function configure() + { + $this + ->setName("generate:views") + ->setDescription("Generate view files for a provided model name.") + ->addArgument( + 'modelName', + InputArgument::REQUIRED, + 'Full qualified model class name' + ) + ->addOption( + 'view', + null, + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, + 'Tells witch view to generate.', + ['index', 'show', 'add', 'edit'] + ) + ->addOption( + 'path', + 'p', + InputOption::VALUE_OPTIONAL, + 'Sets the application path where views are located', + getcwd() + ) + ->addOption( + 'out', + 'o', + InputOption::VALUE_OPTIONAL, + 'The views folder where to save the view templates.', + 'Views' + ); + } + + /** + * Executes the current command. + * + * This method is not abstract because you can use this class + * as a concrete class. In this case, instead of defining the + * execute() method, you set the code to execute by passing + * a Closure to the setCode() method. + * + * @param InputInterface $input An InputInterface instance + * @param OutputInterface $output An OutputInterface instance + * + * @return null|integer null or 0 if everything went fine, or an error code + * + * @throws \LogicException When this abstract method is not implemented + * @see setCode() + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + /** @var Application $application */ + $application = $this->getApplication(); + $output->writeln($application->getLongVersion()); + $output->writeln( + "Generate views for model ". + $input->getArgument('modelName') + ); + $output->writeln(""); + } +} \ No newline at end of file diff --git a/src/Slick/Mvc/Command/Task/GenerateController.php b/src/Slick/Mvc/Command/Task/GenerateController.php new file mode 100644 index 0000000..bff99dc --- /dev/null +++ b/src/Slick/Mvc/Command/Task/GenerateController.php @@ -0,0 +1,52 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.1.0 + */ + +namespace Slick\Mvc\Command\Task; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Generate controller content + * + * @package Slick\Mvc\Command\Utils + * @author Filipe Silva + */ +class GenerateController extends GenerateScaffoldController +{ + + /** + * @readwrite + * @var string + */ + protected $_template = 'templates/controller.twig'; + + /** + * Runs the task + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return bool + */ + public function run(InputInterface $input, OutputInterface $output) + { + parent::run($input, $output); + $formTask = new GenerateForm( + [ + 'command' => $this->_command, + 'controllerData' => $this->_controllerData, + 'path' => $this->getPath() + ] + ); + return $formTask->run($input, $output); + } +} \ No newline at end of file diff --git a/src/Slick/Mvc/Command/Task/GenerateForm.php b/src/Slick/Mvc/Command/Task/GenerateForm.php new file mode 100644 index 0000000..58accc5 --- /dev/null +++ b/src/Slick/Mvc/Command/Task/GenerateForm.php @@ -0,0 +1,54 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.1.0 + */ + +namespace Slick\Mvc\Command\Task; + +/** + * Generate form content + * + * @package Slick\Mvc\Command\Utils + * @author Filipe Silva + */ +class GenerateForm extends GenerateScaffoldController +{ + + /** + * @readwrite + * @var string + */ + protected $_template = 'templates/form.twig'; + + /** + * Returns the form path + * + * @return string + */ + public function getPath() + { + return $this->_path . '/Forms'; + } + + /** + * @var string + */ + protected $_objectType = 'Form'; + + /** + * Returns the file name to output + * + * @return string + */ + protected function _getFileName() + { + return "{$this->_controllerData->getControllerSimpleName()}Form.php"; + } +} \ No newline at end of file diff --git a/src/Slick/Mvc/Command/Task/GenerateScaffoldController.php b/src/Slick/Mvc/Command/Task/GenerateScaffoldController.php new file mode 100644 index 0000000..fc773df --- /dev/null +++ b/src/Slick/Mvc/Command/Task/GenerateScaffoldController.php @@ -0,0 +1,129 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.1.0 + */ + +namespace Slick\Mvc\Command\Task; + +use Slick\Common\Base; +use Slick\FileSystem\Folder; +use Slick\Mvc\Command\Utils\ControllerData; +use Slick\Template\Template; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\DialogHelper; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Generate scaffold controller content + * + * @package Slick\Mvc\Command\Utils + * @author Filipe Silva + * + * @method string getPath() Returns the file path + */ +class GenerateScaffoldController extends Base implements TaskInterface +{ + + /** + * @readwrite + * @var ControllerData + */ + protected $_controllerData; + + /** + * @readwrite + * @var string + */ + protected $_template = 'templates/scaffold-controller.twig'; + + /** + * @readwrite + * @var string + */ + protected $_path; + + /** + * @readwrite + * @var Command + */ + protected $_command; + + /** + * @var string + */ + protected $_objectType = 'Controller'; + + /** + * Set template path + * + * @param array $options + */ + public function __construct(array $options = []) + { + parent::__construct($options); + Template::addPath(dirname(dirname(__DIR__)) .'/Views'); + } + + /** + * Runs the task + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return bool + */ + public function run(InputInterface $input, OutputInterface $output) + { + $template = new Template(); + $template = $template->initialize(); + + $data = ['command' => $this->_controllerData]; + $content = $template->parse($this->_template)->process($data); + + $fileName = $this->_getFileName(); + $folder = new Folder(['name' => $this->getPath()]); + + /** @var DialogHelper $dialog */ + $dialog = $this->_command->getHelperSet()->get('dialog'); + $name = $folder->details->getRealPath() . '/'. $fileName; + $save = true; + + if ($folder->hasFile($fileName)) { + $output->writeln("File '{$name}' already exists."); + if (!$dialog->askConfirmation( + $output, + 'Do you want to override existing file?', + false + )) { + $save = false; + } + } + + if ($save) { + $folder->addFile($fileName) + ->write($content); + $output->writeln("{$this->_objectType} file generated successfully!"); + } else { + $output->writeln("{$this->_objectType} file was not created."); + } + $output->writeln(''); + } + + /** + * Returns the file name to output + * + * @return string + */ + protected function _getFileName() + { + return $this->_controllerData->getControllerSimpleName() .'.php'; + } +} \ No newline at end of file diff --git a/src/Slick/Mvc/Command/Task/TaskInterface.php b/src/Slick/Mvc/Command/Task/TaskInterface.php new file mode 100644 index 0000000..ce3d29e --- /dev/null +++ b/src/Slick/Mvc/Command/Task/TaskInterface.php @@ -0,0 +1,36 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.1.0 + */ + +namespace Slick\Mvc\Command\Task; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Task interface + * + * @package Slick\Mvc\Command\Utils + * @author Filipe Silva + */ +interface TaskInterface +{ + + /** + * Runs the task + * + * @param InputInterface $input + * @param OutputInterface $output + * + * @return bool + */ + public function run(InputInterface $input, OutputInterface $output); +} \ No newline at end of file diff --git a/src/Slick/Mvc/Command/Utils/ControllerData.php b/src/Slick/Mvc/Command/Utils/ControllerData.php new file mode 100644 index 0000000..c177e58 --- /dev/null +++ b/src/Slick/Mvc/Command/Utils/ControllerData.php @@ -0,0 +1,165 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.0.0 + */ + +namespace Slick\Mvc\Command\Utils; + +use Slick\Common\Base; +use Slick\Mvc\Model\Descriptor; +use Slick\Orm\Entity\Manager; +use Slick\Utility\Text; + +/** + * Controller meta data + * + * @package Slick\Mvc\Command\Utils + * @author Filipe Silva + * + * @property string $modelName + * @property string $nameSpace + * @property string $controllerName + * + * @method string getModelName() + */ +class ControllerData extends Base +{ + /** + * @readwrite + * @var string + */ + protected $_controllerName; + + /** + * @readwrite + * @var string + */ + protected $_namespace; + + /** + * @readwrite + * @var string + */ + protected $_modelName; + + /** + * @readwrite + * @var Descriptor + */ + protected $_descriptor; + + /** + * Sets controller namespace + * + * @param string $namespace + * + * @return ControllerData + */ + public function setNameSpace($namespace) + { + $this->_namespace = str_replace('/', '\\', $namespace); + return $this; + } + + /** + * Sets controller name + * + * @param string $modelName + * + * @return ControllerData + */ + public function setControllerName($modelName) + { + $name = end(explode('/', $modelName)); + $this->_controllerName = ucfirst(Text::plural($name)); + return $this; + } + + /** + * Sets model name + * + * @param string $modelName + * + * @return ControllerData + */ + public function setModelName($modelName) + { + $this->_modelName = str_replace('/', '\\', $modelName); + return $this; + } + + /** + * Return controller class name + * + * @return string + */ + public function getControllerSimpleName() + { + return end(explode('\\', $this->controllerName)); + } + + /** + * Returns model class name + * + * @return string + */ + public function getModelSimpleName() + { + return end(explode('\\', $this->modelName)); + } + + /** + * Returns lowercase model name in plural form + * + * @return string + */ + public function getModelPlural() + { + return strtolower(Text::plural($this->getModelSimpleName())); + } + + /** + * Return lowercase model name in singular form + * + * @return string + */ + public function getModelSingular() + { + return strtolower($this->getModelSimpleName()); + } + + /** + * Returns the form name for this controller + * + * @return string + */ + public function getFormName() + { + return $this->getModelSimpleName() .'Form'; + } + + /** + * Returns the model descriptor object + * + * @return Descriptor + */ + public function getDescriptor() + { + if (is_null($this->_descriptor)) { + $this->_descriptor = new Descriptor( + [ + 'descriptor' => Manager::getInstance() + ->get($this->getModelName()) + ] + ); + } + return $this->_descriptor; + } +} diff --git a/src/Slick/Mvc/Controller.php b/src/Slick/Mvc/Controller.php index 77056a5..4486887 100644 --- a/src/Slick/Mvc/Controller.php +++ b/src/Slick/Mvc/Controller.php @@ -31,6 +31,7 @@ * @property bool $renderView Flag for view rendering * @property string $layout Layout file name * @property string $view View file name + * @property bool $scaffold * * @property-read array $viewVars A key/value pair of data for view rendering * @@ -39,6 +40,7 @@ * @method Controller setRequest(Request $request) Sets the HTTP request object * @method Response getResponse() Returns the HTTP response object * @method Controller setResponse(Response $response) Sets HTTP response object + * @method bool isScaffold() Returns true if controller is scaffolding */ class Controller extends Base { @@ -105,6 +107,8 @@ class Controller extends Base * Sends a redirection header and exits execution. * * @param array|string $url The url to redirect to. + * + * @return self */ public function redirect($url) { @@ -113,7 +117,7 @@ public function redirect($url) $this->_response->setStatusCode(302); $header = new GenericHeader('Location', $location); $this->_response->getHeaders()->addHeader($header); - $this->disableRendering(); + return $this->disableRendering(); } /** diff --git a/src/Slick/Mvc/Dispatcher.php b/src/Slick/Mvc/Dispatcher.php index ceb3470..bc21c23 100644 --- a/src/Slick/Mvc/Dispatcher.php +++ b/src/Slick/Mvc/Dispatcher.php @@ -129,8 +129,8 @@ public function dispatch(RouteInfo $routeInfo) $controller, $method ), - is_array($this->_routeInfo->params) ? - $this->_routeInfo->params : [] + is_array($this->_routeInfo->getArguments()) ? + $this->_routeInfo->getArguments() : [] ); $hooks($methodMeta, "@after"); @@ -160,7 +160,13 @@ public function getController() ); /** @var Controller $instance */ - $this->_controller = new $className($options); + $instance = new $className($options); + $this->_controller = $instance; + if ($instance->isScaffold()) { + $this->_controller = Scaffold::getScaffoldController( + $instance + ); + } } return $this->_controller; } @@ -173,7 +179,7 @@ public function getController() protected function _render() { $response = $this->_application->response; - $body = ''; + $body = null; $this->_controller->set( 'flashMessages', $this->_controller->flashMessages ); diff --git a/src/Slick/Mvc/Libs/Utils/Pagination.php b/src/Slick/Mvc/Libs/Utils/Pagination.php new file mode 100644 index 0000000..a5781ab --- /dev/null +++ b/src/Slick/Mvc/Libs/Utils/Pagination.php @@ -0,0 +1,179 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.0.0 + */ + +namespace Slick\Mvc\Libs\Utils; + +use Slick\Common\Base; +use Slick\Filter\StaticFilter; +use Slick\Validator\StaticValidator; +use Zend\Http\PhpEnvironment\Request; + +/** + * Pagination utility + * + * @package Slick\Mvc\Libs\Utils + * @author Filipe Silva + * + * @property Request $request + * @property int $offset + * @property int $rowsPerPage + */ +class Pagination extends Base +{ + + /** + * @readwrite + * @var int Total pages + */ + protected $_pages = 0; + + /** + * @readwrite + * @var int Total records + */ + protected $_total; + + /** + * @readwrite + * @var int current page index + */ + protected $_current = 1; + + /** + * @readwrite + * @var int total rows per page + */ + protected $_rowsPerPage = 12; + + /** + * @readwrite + * @var int First row to return + */ + protected $_offset = 0; + + /** + * @readwrite + * @var Request + */ + protected $_request; + + /** + * Overrides the constructor to calculate the properties for current + * pagination state. + * + * @param array $options + */ + public function __construct($options = array()) + { + parent::__construct($options); + + if ($this->request->getQuery('rows')) { + $this->setRowsPerPage($this->request->getQuery('rows')); + } + + if ($this->request->getQuery('page')) { + $this->setCurrent($this->request->getQuery('page')); + } + + + } + + /** + * Lazy loads request object + * + * @return Request + */ + public function getRequest() + { + if (is_null($this->_request)) { + $this->_request = new Request(); + } + return $this->_request; + } + + /** + * Sets current page and calculates offset + * + * @param integer $value The number of the current page. + * + * @return Pagination A self instance for method chain calls + */ + public function setCurrent($value) + { + if (!StaticValidator::isValid('number', $value)) { + return $this; + } + + $this->_current = StaticFilter::filter('number',$value); + $this->_offset = $this->_rowsPerPage * ($this->_current - 1); + return $this; + } + + /** + * Sets the total rows to paginate. + * + * @param integer $value The rows total to set. + * + * @return Pagination A self instance for method chain calls + */ + public function setTotal($value) + { + if (!StaticValidator::isValid('number', $value)) { + return $this; + } + + $this->_total = StaticFilter::filter('number',$value); + $this->_pages = ceil($this->_total / $this->_rowsPerPage); + return $this; + } + + /** + * Sets the total rows per page. + * + * @param integer $value The total rows per page to set. + * + * @return Pagination A self instance for method chain calls + */ + public function setRowsPerPage($value) + { + if (!StaticValidator::isValid('number', $value)) { + return $this; + } + + $this->_rowsPerPage = StaticFilter::filter('number',$value); + $this->_pages = ceil($this->_total / $this->_rowsPerPage); + return $this; + } + + /** + * Creates a request query for the provided page. + * + * This method check the current request query in order to maintain the + * other parameters unchanged and sets the 'page' parameter to the + * provided page number. + * + * @param integer $page The page number to build the query on. + * + * @return string The query string to use in the pagination links. + */ + public function pageUrl($page) + { + $params = $this->request->getQuery(); + + if (isset($params['url'])) + unset($params['url']); + if (isset($params['extension'])) + unset($params['extension']); + $params['page'] = $page; + return '?' . http_build_query($params); + } +} diff --git a/src/Slick/Mvc/Model/Descriptor.php b/src/Slick/Mvc/Model/Descriptor.php index 943a4a2..f300aa7 100644 --- a/src/Slick/Mvc/Model/Descriptor.php +++ b/src/Slick/Mvc/Model/Descriptor.php @@ -13,7 +13,10 @@ namespace Slick\Mvc\Model; use Slick\Common\Base; +use Slick\Orm\Annotation\Column; use Slick\Orm\Entity\Descriptor as SlickOrmDescriptor; +use Slick\Orm\RelationInterface; +use Slick\Utility\Text; /** * MVC Model descriptor @@ -39,6 +42,30 @@ class Descriptor extends Base */ protected $_descriptor; + /** + * @readwrite + * @var array + */ + protected $_modelPlural = []; + + /** + * @readwrite + * @var array + */ + protected $_modelSingular = []; + + /** + * @readwrite + * @var string + */ + protected $_primaryKey; + + /** + * @readwrite + * @var string + */ + protected $_tableName; + /** * Returns the display field name * @@ -76,4 +103,117 @@ public function getDisplayField() return $this->_displayField; } + + /** + * Returns the list of entity columns + * + * @return Column[] + */ + public function getColumns() + { + return $this->getDescriptor()->getColumns(); + } + + /** + * Returns the list of relations of current entity + * + * @return RelationInterface[] + */ + public function getRelations() + { + return $this->getDescriptor()->getRelations(); + } + + /** + * Returns the relation defined in the provided property name, or false + * if there is no relation defined with that name. + * + * @param string $name + * + * @return bool|RelationInterface + */ + public function getRelation($name) + { + return $this->getDescriptor()->getRelation($name); + } + + /** + * Returns the plural form of the class name + * + * @param RelationInterface $relation + * + * @return string + */ + public function modelPlural(RelationInterface $relation) + { + if (!isset($this->_modelPlural[$relation->getPropertyName()])) { + $parts = explode('\\', $relation->getRelatedEntity()); + $name = strtolower(end($parts)); + $this->_modelPlural[$relation->getPropertyName()] = Text::plural($name); + } + return $this->_modelPlural[$relation->getPropertyName()]; + } + + /** + * Returns the singular form of the class name + * + * @param RelationInterface $relation + * + * @return string + */ + public function modelSingular(RelationInterface $relation) + { + if (!isset($this->_modelSingular[$relation->getPropertyName()])) { + $parts = explode('\\', $relation->getRelatedEntity()); + $this->_modelSingular[$relation->getPropertyName()] = strtolower(end($parts)); + } + return $this->_modelSingular[$relation->getPropertyName()]; + } + + /** + * Returns model primary key name + * + * @return string + */ + public function getPrimaryKey() + { + if (is_null($this->_primaryKey)) { + $this->_primaryKey = $this->getDescriptor() + ->getEntity()->getPrimaryKey(); + } + return $this->_primaryKey; + } + + /** + * Returns the column of a given relation + * + * @param RelationInterface $relation + * + * @return Descriptor + */ + public function getRelationDescriptor(RelationInterface $relation) + { + return Manager::getInstance() + ->get( + new SlickOrmDescriptor( + [ + 'entity' => $relation->getRelatedEntity() + ] + ) + ); + } + + /** + * Returns model table name + * + * @return string + */ + public function getTableName() + { + if (is_null($this->_tableName)) { + $this->_tableName = $this->getDescriptor()->getEntity() + ->getTableName(); + } + return $this->_tableName; + } } diff --git a/src/Slick/Mvc/Router/RouteInfo.php b/src/Slick/Mvc/Router/RouteInfo.php index 271650f..93a1ed6 100644 --- a/src/Slick/Mvc/Router/RouteInfo.php +++ b/src/Slick/Mvc/Router/RouteInfo.php @@ -73,7 +73,7 @@ class RouteInfo extends Base * @read * @var array */ - protected $_arguments; + protected $_arguments = []; /** * @read @@ -180,14 +180,20 @@ public function getAction() */ public function getArguments() { - if (is_null($this->_arguments)) { - $names = ['controller', 'action', 'namespace']; + if (empty($this->_arguments)) { + $base = []; + if (isset($this->_params['trailing'])) { + $base = explode('/', $this->_params['trailing']); + } + $names = ['controller', 'action', 'namespace', 'trailing']; foreach ($this->_params as $key => $value) { if (in_array($key, $names)) { continue; } $this->_arguments[$key] = $value; } + + $this->_arguments = array_merge($base, $this->_arguments); } return $this->_arguments; } diff --git a/src/Slick/Mvc/Scaffold.php b/src/Slick/Mvc/Scaffold.php new file mode 100644 index 0000000..a4f4598 --- /dev/null +++ b/src/Slick/Mvc/Scaffold.php @@ -0,0 +1,365 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.1.0 + */ + +namespace Slick\Mvc; + +use Slick\Database; +use Slick\Mvc\Scaffold\Form; +use Slick\Utility\Text; +use Slick\Orm\Sql\Select; +use Slick\Template\Template; +use Slick\Orm\Entity\Manager; +use Slick\Filter\StaticFilter; +use Slick\Mvc\Model\Descriptor; +use Slick\Mvc\Libs\Utils\Pagination; + +/** + * Scaffold controller + * + * @package Slick\Mvc + * @author Filipe Silva + * + * @property Controller $controller + * @property string $scaffoldControllerName + * @property string $modelName + * @property Descriptor $descriptor + * + * @method Controller getController() Returns the controller being scaffold + */ +class Scaffold extends Controller +{ + + /** + * @readwrite + * @var Controller + */ + protected $_controller; + + /** + * @readwrite + * @var string + */ + protected $_modelName; + + /** + * @readwrite + * @var string + */ + protected $_scaffoldControllerName; + + /** + * @readwrite + * @var Descriptor + */ + protected $_descriptor; + + /** + * Set common variables for views + * + * @param array $options + */ + public function __construct($options = []) + { + parent::__construct($options); + $nameParts = explode("\\", get_class($this->_controller)); + $this->_scaffoldControllerName = end($nameParts); + $this->set('modelPlural', strtolower(end($nameParts))); + $this->set( + 'modelSingular', + Text::singular(strtolower(end($nameParts))) + ); + Template::appendPath(__DIR__ . '/Views'); + } + + /** + * Creates a new scaffold controller + * + * @param Controller $instance + * @param array $options + * + * @return self + */ + public static function getScaffoldController( + Controller $instance, $options = []) + { + + $options = array_merge( + [ + 'controller' => $instance, + 'request' => $instance->request, + 'response' => $instance->response + ], + $options + ); + return new static($options); + } + + /** + * Returns the model class name + * + * @return string + */ + public function getModelName() + { + if (is_null($this->_modelName)) { + $this->setModelName('Models\\' . + ucfirst( + Text::singular( + strtolower($this->_scaffoldControllerName) + ) + ) + ); + } + return $this->_modelName; + } + + /** + * Sets model name + * + * @param string $name + * + * @return self + */ + public function setModelName($name) + { + $this->_modelName = $name; + $nameParts = explode("\\", $name); + $controllerName = strtolower(end($nameParts)); + $this->set('modelPlural', strtolower(Text::plural($controllerName))); + $this->set( + 'modelSingular', + strtolower(Text::singular($controllerName)) + ); + return $this; + } + + /** + * Returns model descriptor + * + * @return Descriptor + */ + public function getDescriptor() + { + if (is_null($this->_descriptor)) { + $this->_descriptor = new Descriptor( + [ + 'descriptor' => Manager::getInstance() + ->get($this->getModelName()) + ] + ); + } + return $this->_descriptor; + } + + /** + * Handles the request to display index page + */ + public function index() + { + $pagination = new Pagination(); + $pattern = StaticFilter::filter( + 'text', + $this->getController()->request->getQuery('pattern', null) + ); + $this->view = 'scaffold/index'; + $descriptor = $this->getDescriptor(); + + /** @var Select $query */ + $query = call_user_func_array([$this->getModelName(), 'find'], []); + $field = $descriptor->getDisplayField(); + $tableName = $descriptor->getDescriptor()->getEntity()->getTableName(); + $query->where( + [ + "{$tableName}.{$field} LIKE :pattern" => [ + ':pattern' => "%{$pattern}%" + ] + ] + ); + $pagination->setTotal($query->count()); + $query->limit( + $pagination->rowsPerPage, + $pagination->offset + ); + $records = $query->all(); + $this->set(compact('pagination', 'records', 'pattern', 'descriptor')); + } + + /** + * Handles the request to display show page + * + * @param int $recordId + */ + public function show($recordId = 0) + { + $this->view = 'scaffold/show'; + $recordId = StaticFilter::filter('number', $recordId); + + $record = call_user_func_array( + [$this->getModelName(), 'get'], + [$recordId] + ); + + if (is_null($record)) { + $this->addWarningMessage( + "The {$this->get('modelSingular')} with the provided key ". + "does not exists." + ); + + $this->redirect($this->get('modelPlural')); + return; + } + $descriptor = $this->getDescriptor(); + $this->set(compact('record', 'descriptor')); + } + + /** + * Handles the request to add page + */ + public function add() + { + $this->view = 'scaffold/add'; + $form = new Form( + "add-{$this->get('modelSingular')}", $this->getDescriptor() + ); + if ($this->request->isPost()) { + $form->setData($this->request->getPost()); + if ($form->isValid()) { + try { + $modelClass = $this->getModelName(); + /** @var Model $model */ + $model = new $modelClass($form->getValues()); + $model->save(); + $name = ucfirst($this->get('modelSingular')); + $this->addSuccessMessage( + "{$name} successfully created." + ); + $pmk = $model->getPrimaryKey(); + $this->redirect( + $this->get('modelPlural').'/show/'.$model->$pmk + ); + return; + } catch (Database\Exception $exp) { + $this->addErrorMessage( + "Error while saving {$this->get('modelSingular')}} " . + "data: {$exp->getMessage()}" + ); + } + } else { + $this->addErrorMessage( + "Cannot save {$this->get('modelSingular')}. " . + "Please correct the errors bellow." + ); + } + } + $descriptor = $this->getDescriptor(); + $this->set(compact('form', 'descriptor')); + } + + /** + * Handles the request to edit page + * + * @param int $recordId + */ + public function edit($recordId = 0) + { + $this->view = 'scaffold/edit'; + $recordId = StaticFilter::filter('number', $recordId); + + /** @var Model $record */ + $record = call_user_func_array( + [$this->getModelName(), 'get'], + [$recordId] + ); + + if (is_null($record)) { + $this->addWarningMessage( + "The {$this->get('modelSingular')} with the provided key ". + "does not exists." + ); + + $this->redirect($this->get('modelPlural')); + return; + } + + $form = new Form( + "edit-{$this->get('modelSingular')}", $this->getDescriptor() + ); + + if ($this->request->isPost()) { + $form->setData($this->request->getPost()); + if ($form->isValid()) { + try { + $modelClass = $this->getModelName(); + /** @var Model $model */ + $model = new $modelClass($form->getValues()); + $model->save(); + $name = ucfirst($this->get('modelSingular')); + $this->addSuccessMessage( + "{$name} successfully updated." + ); + $pmk = $model->getPrimaryKey(); + $this->redirect( + $this->get('modelPlural').'/show/'.$model->$pmk + ); + return; + } catch (Database\Exception $exp) { + $this->addErrorMessage( + "Error while saving {$this->get('modelSingular')}} " . + "data: {$exp->getMessage()}" + ); + } + } else { + $this->addErrorMessage( + "Cannot save {$this->get('modelSingular')}. " . + "Please correct the errors bellow." + ); + } + } else { + $form->setData($record->asArray()); + } + $descriptor = $this->getDescriptor(); + $this->set(compact('form', 'record', 'descriptor')); + } + + /** + * Handles the request to delete a record + */ + public function delete() + { + if ($this->request->isPost()) { + $recordId = StaticFilter::filter( + 'text', + $this->request->getPost('id') + ); + + $record = call_user_func_array( + [$this->getModelName(), 'get'], + [$recordId] + ); + + if (is_null($record)) { + $this->addWarningMessage( + "The {$this->get('modelSingular')} with the provided key ". + "does not exists." + ); + } else { + if ($record->delete()) { + $this->addSuccessMessage( + "The {$this->get('modelSingular')} was successfully " . + "deleted." + ); + } + } + } + return $this->redirect($this->get('modelPlural')); + } +} diff --git a/src/Slick/Mvc/Scaffold/Form.php b/src/Slick/Mvc/Scaffold/Form.php new file mode 100644 index 0000000..9907928 --- /dev/null +++ b/src/Slick/Mvc/Scaffold/Form.php @@ -0,0 +1,189 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.1.0 + */ + +namespace Slick\Mvc\Scaffold; + +use Slick\Common\Inspector; +use Slick\Form\Element\Submit; +use Slick\Orm\Relation\HasMany; +use Slick\Orm\Relation\HasOne; +use Slick\Mvc\Model\Descriptor; +use Slick\Orm\Annotation\Column; +use Slick\Orm\RelationInterface; +use Slick\Form\Form as SlickFrom; +use Slick\Orm\Relation\HasAndBelongsToMany; + +/** + * Form + * + * @package Slick\Mvc\Router + * @author Filipe Silva + */ +class Form extends SlickFrom +{ + + /** + * @read + * @var Descriptor + */ + protected $_descriptor; + + /** + * @var Column[] + */ + private $_columns = []; + + /** + * @var RelationInterface[] + */ + private $_relations = []; + + /** + * @var array + */ + private $_properties = []; + + /** + * @var Inspector + */ + private $_inspector; + + /** + * @var array + */ + private $_validations = [ + 'notEmpty', 'email', 'url', 'number', 'alphaNumeric' + ]; + + /** + * @var array + */ + private $_filters = [ + 'text', 'htmlEntities', 'number', 'url' + ]; + + /** + * Add elements to the form based on the model notations + * + * @param string $name + * @param Descriptor $descriptor + */ + public function __construct($name, Descriptor $descriptor) + { + $this->_descriptor = $descriptor; + $this->_columns = $this->_descriptor->getColumns(); + $this->_relations = $this->_descriptor->getRelations(); + $this->_inspector = new Inspector( + $this->_descriptor->getDescriptor()->getEntity() + ); + $this->_properties = $this->_inspector->getClassProperties(); + parent::__construct($name); + } + + /** + * Callback for form setup + */ + protected function _setup() + { + foreach ($this->_properties as $property) { + $element = $this->_createElement($property); + if ($element) { + $this->addElement($element['name'], $element); + } + } + $this->add( + new Submit( + ['value' => 'Save'] + ) + ); + } + + protected function _createElement($property) + { + if (isset($this->_columns[$property])) { + return $this->_createFromColumn( + $property, + $this->_columns[$property] + ); + } + if (isset($this->_relations[$property])) { + return $this->_createFromRelation( + $property, + $this->_relations[$property] + ); + } + return false; + } + + protected function _createFromColumn($property, Column $column) + { + $name = trim($property, '_'); + $options = [ + 'name' => $name, + 'label' => ucfirst($name), + 'type' => 'text' + ]; + $this->_addValidateOptions($property, $options); + if ($column->getParameter('primaryKey')) { + $options['type'] = 'hidden'; + } + if ($column->getParameter('size') == 'big') { + $options['type'] = 'area'; + } + if ($column->getParameter('type') == 'boolean') { + $options['type'] = 'checkbox'; + } + if ($column->getParameter('type') == 'datetime') { + $options['type'] = 'dateTime'; + } + return $options; + } + + protected function _createFromRelation( + $property, RelationInterface $relation) + { + if (($relation instanceof HasOne) || ($relation instanceof HasMany)) { + return false; + } + $name = trim($property, '_'); + $options = [ + 'name' => $name, + 'label' => ucfirst($name), + 'type' => 'select' + ]; + if ($relation instanceof HasAndBelongsToMany) { + $options['type'] = 'selectMultiple'; + } + $this->_addValidateOptions($property, $options); + $optionsList = call_user_func( + [$relation->getRelatedEntity(), 'getList'] + ); + $options['options'] = $optionsList; + + return $options; + } + + protected function _addValidateOptions($property, array &$options) + { + $metaData = $this->_inspector->getPropertyAnnotations($property); + if ($metaData->hasAnnotation('@validate')) { + /** @var Inspector\Annotation $annotation */ + $annotation = $metaData->getAnnotation('@validate'); + $validators = $annotation->allValues(); + foreach ($validators as $validator) { + if (in_array($validator, $this->_validations)) { + $options['validate'][] = $validator; + } + } + } + } +} diff --git a/src/Slick/Mvc/Views/elements/delete.html.twig b/src/Slick/Mvc/Views/elements/delete.html.twig new file mode 100644 index 0000000..a773484 --- /dev/null +++ b/src/Slick/Mvc/Views/elements/delete.html.twig @@ -0,0 +1,26 @@ +{% macro deleteMacro(link, record, element, modelName)%} + + +{% endmacro %} \ No newline at end of file diff --git a/src/Slick/Mvc/Views/elements/pagination.html.twig b/src/Slick/Mvc/Views/elements/pagination.html.twig new file mode 100644 index 0000000..472628d --- /dev/null +++ b/src/Slick/Mvc/Views/elements/pagination.html.twig @@ -0,0 +1,105 @@ + + diff --git a/src/Slick/Mvc/Views/elements/panel-heading.twig b/src/Slick/Mvc/Views/elements/panel-heading.twig new file mode 100644 index 0000000..1a74a86 --- /dev/null +++ b/src/Slick/Mvc/Views/elements/panel-heading.twig @@ -0,0 +1,25 @@ +
+
+
+
+
+ + + +
+
+
+ + + +
+
\ No newline at end of file diff --git a/src/Slick/Mvc/Views/elements/table-record.actions.html.twig b/src/Slick/Mvc/Views/elements/table-record.actions.html.twig new file mode 100644 index 0000000..846336b --- /dev/null +++ b/src/Slick/Mvc/Views/elements/table-record.actions.html.twig @@ -0,0 +1,39 @@ +{% macro getActions(record, path, name) %} +
+
+ + + {{ translate("Edit") }} + + + +
+
+{% endmacro %} + +{% macro createDialog(record, path, name) %} + {% import "elements/delete.html.twig" as modal %} + {{ modal.deleteMacro( + url(path ~ '/delete/' ~ record.getKey()), + record, + "delete" ~ record.getKey(), + name + ) }} +{% endmacro %} \ No newline at end of file diff --git a/src/Slick/Mvc/Views/flash/messages.html.twig b/src/Slick/Mvc/Views/flash/messages.html.twig new file mode 100644 index 0000000..b3bc2ef --- /dev/null +++ b/src/Slick/Mvc/Views/flash/messages.html.twig @@ -0,0 +1,13 @@ +{% set messages = flashMessages.get %} +{% if messages|length > 0 %} +
+ {% for type, especific in messages %} + {% for message in especific %} +
+ + {{ message|raw }} +
+ {% endfor %} + {% endfor %} +
+{% endif %} \ No newline at end of file diff --git a/src/Slick/Mvc/Views/scaffold/add.html.twig b/src/Slick/Mvc/Views/scaffold/add.html.twig new file mode 100644 index 0000000..3723c20 --- /dev/null +++ b/src/Slick/Mvc/Views/scaffold/add.html.twig @@ -0,0 +1,11 @@ +
+

+ Add new {{ modelSingular }} +

+ + {% include 'scaffold/quick-links.twig' with {"page": "add"} %} + +
+ {{ form.render|raw }} +
+
\ No newline at end of file diff --git a/src/Slick/Mvc/Views/scaffold/edit.html.twig b/src/Slick/Mvc/Views/scaffold/edit.html.twig new file mode 100644 index 0000000..498628a --- /dev/null +++ b/src/Slick/Mvc/Views/scaffold/edit.html.twig @@ -0,0 +1,9 @@ +
+

Edit {{ record }}

+ + {% include 'scaffold/quick-links.twig' with {"page": "edit"} %} + +
+ {{ form.render|raw }} +
+
diff --git a/src/Slick/Mvc/Views/scaffold/index.html.twig b/src/Slick/Mvc/Views/scaffold/index.html.twig new file mode 100644 index 0000000..fe34bfc --- /dev/null +++ b/src/Slick/Mvc/Views/scaffold/index.html.twig @@ -0,0 +1,75 @@ +{% import "elements/table-record.actions.html.twig" as tableRecord %} +
+

+ + {{ modelPlural|capitalize }} +

+ + {% include 'scaffold/quick-links.twig' with {"page": "index"} %} + +
+ {% include 'elements/panel-heading.twig' %} +
+ {% if records|length > 0 %} + + + + {% for column in descriptor.getColumns if not column.getParameter('primaryKey')%} + {% if loop.first %} + + {% else %} + + {% endif %} + {% endfor %} + {% for relation in descriptor.getRelations if relation.isSingleResult %} + + {% endfor %} + + + + + {% for record in records %} + + {% for column in descriptor.getColumns if not column.getParameter('primaryKey')%} + {% if loop.first %} + + {% else %} + + {% endif %} + {% endfor %} + {% for relation in descriptor.getRelations if relation.isSingleResult %} + + {% endfor %} + + + {% endfor %} + +
+ {{ column.field|capitalize }} + + Actions +
+ + {{ attribute(record, column.field)|truncate(150, true)|wordwrap(80, '
')|raw}} +
+
{{ tableRecord.getActions(record, modelPlural, modelSingular) }}
+ {% else %} + There are no {{ modelPlural }} to show up here. Click the Add {{ modelSingular }} button to add a new {{ modelSingular }}. + {% endif %} +
+ +
+
+{% for record in records %} + {{ tableRecord.createDialog(record, modelPlural, modelSingular) }} +{% endfor %} \ No newline at end of file diff --git a/src/Slick/Mvc/Views/scaffold/quick-links.twig b/src/Slick/Mvc/Views/scaffold/quick-links.twig new file mode 100644 index 0000000..53a53b9 --- /dev/null +++ b/src/Slick/Mvc/Views/scaffold/quick-links.twig @@ -0,0 +1,27 @@ + \ No newline at end of file diff --git a/src/Slick/Mvc/Views/scaffold/show.html.twig b/src/Slick/Mvc/Views/scaffold/show.html.twig new file mode 100644 index 0000000..cda8193 --- /dev/null +++ b/src/Slick/Mvc/Views/scaffold/show.html.twig @@ -0,0 +1,147 @@ +{% import "elements/table-record.actions.html.twig" as tableRecord %} +
+

+ View {{ record }} +

+ + {% include 'scaffold/quick-links.twig' with {"page": "show"} %} + +
+
+
+
+ {% for column in descriptor.getColumns if not column.getParameter('primaryKey')%} +
{{ column.field|capitalize }}:
+
{{ attribute(record, column.field)|wordwrap(80, '
')|raw|nl2br }}
+ {% endfor %} +
+
+
+
+ + +  Edit {{ modelSingular }} + +   + + +  Delete {{ modelSingular }} + + + {{ tableRecord.createDialog(record, modelPlural, modelSingular) }} + +
+
+
+ + {% if descriptor.getRelations|length > 0 %} + + {% endif %} +
\ No newline at end of file diff --git a/src/Slick/Mvc/Views/templates/controller.twig b/src/Slick/Mvc/Views/templates/controller.twig new file mode 100644 index 0000000..19b625d --- /dev/null +++ b/src/Slick/Mvc/Views/templates/controller.twig @@ -0,0 +1,209 @@ +{{ ''|raw }} + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace {{ command.getNamespace }}; + +use {{ command.getModelName|raw }}; +use Slick\Database; +use Slick\Mvc\Controller; +use Slick\Filter\StaticFilter; +use Slick\Mvc\Libs\Utils\Pagination; + +/** + * {{ command.getControllerSimpleName }} controller + * + * @package {{ command.getNamespace|raw }} + * @author Your Name {{ ''|raw }} + */ +class {{ command.getControllerSimpleName }} extends Controller +{ + +{% block body %} + /** + * Handles the request to display index page + */ + public function index() + { + $pagination = new Pagination(); + $pattern = StaticFilter::filter( + 'text', + $this->request->getQuery('pattern', null) + ); + + $query = {{ command.getModelSimpleName }}::find() + ->where( + [ + "{{ command.descriptor.tableName }}.{{ command.descriptor.displayField }} LIKE :pattern" => [ + ':pattern' => "%{$pattern}%" + ] + ] + ); + + $pagination->setTotal($query->count()); + $query->limit( + $pagination->rowsPerPage, + $pagination->offset + ); + + ${{ command.getModelPlural }} = $query->all(); + $this->set(compact('pagination', '{{ command.getModelPlural }}', 'pattern')); + } + + /** + * Handles the request to display show page + * + * @param int ${{ command.getModelSingular }}Id + */ + public function show(${{ command.getModelSingular }}Id = 0) + { + ${{ command.getModelSingular }}Id = StaticFilter::filter('number', ${{ command.getModelSingular }}Id); + ${{ command.getModelSingular }} = {{ command.getModelSimpleName }}::get(${{ command.getModelSingular }}Id); + + if (is_null(${{ command.getModelSingular }})) { + $this->addWarningMessage( + $this->translate( + "The {{ command.getModelSingular }} with the provided key does not exists." + ) + ); + $this->redirect('{{ command.getModelPlural }}'); + return; + } + $this->set(compact('{{ command.getModelSingular }}')); + } + + /** + * Handles the request to add page + */ + public function add() + { + $form = new Forms\{{ command.getModelSimpleName }}Form('add-{{ command.getModelSimpleName }}'); + if ($this->request->isPost()) { + $form->setData($this->request->getPost()); + if ($form->isValid()) { + try { + ${{ command.getModelSingular }} = new {{ command.getModelSimpleName }}($form->getValues()); + ${{ command.getModelSingular }}->save(); + $this->addSuccessMessage( + $this->translate( + "{{ command.getModelSingular|capitalize }} successfully created." + ) + ); + $this->redirect('{{ command.getModelPlural }}/show/' . ${{ command.getModelSingular }}->{{ command.descriptor.primaryKey }}); + return; + } catch (Database\Exception $exp) { + $this->addErrorMessage( + $this->translate( + "Error while saving {{ command.getModelSingular }} data: {$exp->getMessage()}." + ) + ); + } + } else { + $this->addErrorMessage( + $this->translate( + "Cannot save {{ command.getModelSingular }}. Please correct the errors bellow." + ) + ); + } + } + $this->set(compact('form')); + } + + /** + * Handles the request to edit page + * + * @param int ${{ command.getModelSingular }}Id + */ + public function edit(${{ command.getModelSingular }}Id = 0) + { + ${{ command.getModelSingular }}Id = StaticFilter::filter('number', ${{ command.getModelSingular }}Id); + ${{ command.getModelSingular }} = {{ command.getModelSimpleName }}::get(${{ command.getModelSingular }}Id); + + if (is_null(${{ command.getModelSingular }})) { + $this->addWarningMessage( + $this->translate( + "The {{ command.getModelSingular }} with the provided key does not exists." + ) + ); + $this->redirect('{{ command.getModelPlural }}'); + return; + } + + $form = new Forms\{{ command.getModelSimpleName }}Form('edit-{{ command.getModelSimpleName }}'); + + if ($this->request->isPost()) { + $form->setData($this->request->getPost()); + if ($form->isValid()) { + try { + ${{ command.getModelSingular }} = new {{ command.getModelSimpleName }}($form->getValues()); + ${{ command.getModelSingular }}->save(); + $this->addSuccessMessage( + $this->translate( + "{{ command.getModelSingular|capitalize }} successfully created." + ) + ); + $this->redirect('{{ command.getModelPlural }}/show/' . ${{ command.getModelSingular }}->{{ command.descriptor.primaryKey }}); + return; + } catch (Database\Exception $exp) { + $this->addErrorMessage( + $this->translate( + "Error while saving {{ command.getModelSingular }} data: {$exp->getMessage()}." + ) + ); + } + } else { + $this->addErrorMessage( + $this->translate( + "Cannot update {{ command.getModelSingular }}. Please correct the errors bellow." + ) + ); + } + } else { + $form->setData(${{ command.getModelSingular }}->asArray()); + } + + $this->set(compact('form', '{{ command.getModelSingular }}')); + } + + /** + * Handles the request to delete a record + */ + public function delete() + { + if ($this->request->isPost()) { + ${{ command.getModelSingular }}Id = StaticFilter::filter( + 'number', + $this->request->getPost('id', 0) + ); + + ${{ command.getModelSingular }} = {{ command.getModelSimpleName }}::get(${{ command.getModelSingular }}Id); + + if (is_null(${{ command.getModelSingular }})) { + $this->addWarningMessage( + $this->translate( + "The {{ command.getModelSingular }} with the provided key does not exists." + ) + ); + } else { + try { + ${{ command.getModelSingular }}->delete(); + $this->addSuccessMessage( + "The {{ command.getModelSingular }} was successfully deleted." + ); + } catch (Database\Exception $exp) { + $this->translate( + "Error while deleting {{ command.getModelSingular }} data: {$exp->getMessage()}." + ); + } + } + } + return $this->redirect('{{ command.getModelPlural }}'); + } +{% endblock %} +} diff --git a/src/Slick/Mvc/Views/templates/form.twig b/src/Slick/Mvc/Views/templates/form.twig new file mode 100644 index 0000000..f4b87b0 --- /dev/null +++ b/src/Slick/Mvc/Views/templates/form.twig @@ -0,0 +1,24 @@ +{{ ''|raw }} + * @license http://www.opensource.org/licenses/mit-license.php MIT License + */ + +namespace {{ command.getNamespace|raw }}\Forms; + +use Slick\Form\Element; +use Slick\Form\Form as SlickFrom; + +/** + * {{ command.getModelSingular|capitalize }}Form + * + * @package {{ command.getNamespace|raw }}\Forms + * @author Your Name {{ ''|raw }} + */ +class {{ command.getModelSingular|capitalize }}Form extends SlickFrom +{ +} diff --git a/src/Slick/Mvc/Views/templates/scaffold-controller.twig b/src/Slick/Mvc/Views/templates/scaffold-controller.twig new file mode 100644 index 0000000..0f5bb03 --- /dev/null +++ b/src/Slick/Mvc/Views/templates/scaffold-controller.twig @@ -0,0 +1,31 @@ +{{ ''|raw }} +* @license http://www.opensource.org/licenses/mit-license.php MIT License +*/ + +namespace {{ command.getNamespace }}; + +use Slick\Mvc\Controller; + +/** +* {{ command.getControllerSimpleName }} controller +* +* @package {{ command.getNamespace|raw }} +* @author Your Name {{ ''|raw }} +*/ +class {{ command.getControllerSimpleName }} extends Controller +{ + +{% block body %} + /** + * @readwrite + * @var boolean + */ + protected $_scaffold = true; // Enables the scaffold for this controller. +{% endblock %} +} \ No newline at end of file diff --git a/src/Slick/Orm/Annotation/Column.php b/src/Slick/Orm/Annotation/Column.php index f80150e..285bbe7 100644 --- a/src/Slick/Orm/Annotation/Column.php +++ b/src/Slick/Orm/Annotation/Column.php @@ -20,10 +20,17 @@ * @package Slick\Orm\Annotation * @author Filipe Silva * @author Filipe Silva + * + * @property string $field */ class Column extends Annotation { + /** + * @readwrite + * @var string + */ + protected $_field; /** * Creates an annotation with provided data * diff --git a/src/Slick/Orm/Entity/Descriptor.php b/src/Slick/Orm/Entity/Descriptor.php index f7f59ec..e13f61d 100644 --- a/src/Slick/Orm/Entity/Descriptor.php +++ b/src/Slick/Orm/Entity/Descriptor.php @@ -78,6 +78,7 @@ public function getColumns() if ($annotations->hasAnnotation('column')) { $this->_columns[$property] = $annotations->getAnnotation('column'); + $this->_columns[$property]->field = trim($property, '_'); } } } diff --git a/src/Slick/Orm/Relation/AbstractMultipleRelation.php b/src/Slick/Orm/Relation/AbstractMultipleRelation.php index 9205ac1..c5f480a 100644 --- a/src/Slick/Orm/Relation/AbstractMultipleRelation.php +++ b/src/Slick/Orm/Relation/AbstractMultipleRelation.php @@ -27,6 +27,12 @@ abstract class AbstractMultipleRelation extends AbstractRelation */ protected $_limit = 100; + /** + * @readwrite + * @var bool + */ + protected $_singleResult = false; + /** * Returns the limit rows retrieved with this relation * diff --git a/src/Slick/Orm/Relation/AbstractRelation.php b/src/Slick/Orm/Relation/AbstractRelation.php index 40c83c5..51d9e7d 100644 --- a/src/Slick/Orm/Relation/AbstractRelation.php +++ b/src/Slick/Orm/Relation/AbstractRelation.php @@ -28,7 +28,10 @@ * @author Filipe Silva * * @property string|array $conditions Extra load conditions. + * @property bool $singleResult * + * @method bool isSingleResult() Returns true if this relation returns + * a single result * @method AbstractRelation setContainer(Container $container) * Sets dependency container * @method AbstractRelation setConditions($conditions) Sets an extra @@ -79,6 +82,12 @@ abstract class AbstractRelation extends Base implements RelationInterface */ protected $_conditions; + /** + * @readwrite + * @var bool + */ + protected $_singleResult = true; + /** * Returns the entity that defines the relation * diff --git a/src/Slick/Orm/Relation/HasAndBelongsToMany.php b/src/Slick/Orm/Relation/HasAndBelongsToMany.php index 0828a6a..ea506c9 100644 --- a/src/Slick/Orm/Relation/HasAndBelongsToMany.php +++ b/src/Slick/Orm/Relation/HasAndBelongsToMany.php @@ -200,6 +200,7 @@ public function afterSave(Save $event) $relPrk = Entity\Manager::getInstance()->get($this->getRelatedEntity()) ->getEntity()->getPrimaryKey(); $entPrk = $entity->getPrimaryKey(); + if (isset($entity->$prop) && is_array($entity->$prop)) { $this->beforeDelete(new Delete(['primaryKey' => $entity->$entPrk])); foreach ($entity->$prop as $object) { @@ -207,6 +208,7 @@ public function afterSave(Save $event) if ($object instanceof Entity) { $relVal = $object->$relPrk; } + Sql::createSql($entity->getAdapter()) ->insert($this->getRelationTable()) ->set( diff --git a/src/Slick/Orm/Relation/HasMany.php b/src/Slick/Orm/Relation/HasMany.php index 64286ab..0950a53 100644 --- a/src/Slick/Orm/Relation/HasMany.php +++ b/src/Slick/Orm/Relation/HasMany.php @@ -77,11 +77,11 @@ public function load(Entity $entity) array($this->getRelatedEntity(), 'find'), [] ); - $pmk = $this->getEntity()->getPrimaryKey(); + $pmk = $entity->getPrimaryKey(); $sql->where( [ "{$this->getForeignKey()} = :id" => [ - ':id' => $this->getEntity()->$pmk + ':id' => $entity->$pmk ] ] ); diff --git a/src/Slick/Orm/Sql/Select.php b/src/Slick/Orm/Sql/Select.php index f5aaf1b..861af31 100644 --- a/src/Slick/Orm/Sql/Select.php +++ b/src/Slick/Orm/Sql/Select.php @@ -67,7 +67,7 @@ public function all() $events->trigger(SelectEvent::AFTER_SELECT, $this->_entity, $event); $class = $this->_entity->getClassName(); $recordList = new RecordList(); - foreach ($result as $row) { + foreach ($event->data as $row) { $recordList[] = new $class($row); } return $recordList; diff --git a/src/Slick/Session/Driver/Server.php b/src/Slick/Session/Driver/Server.php index 739b3d7..b2b322c 100644 --- a/src/Slick/Session/Driver/Server.php +++ b/src/Slick/Session/Driver/Server.php @@ -63,6 +63,6 @@ public function __construct($options = array()) */ public function __destruct() { - session_commit(); + //session_commit(); } } \ No newline at end of file diff --git a/src/Slick/Template/Engine/Twig/SlickTwigExtension.php b/src/Slick/Template/Engine/Twig/SlickTwigExtension.php index 2217649..235a4e2 100644 --- a/src/Slick/Template/Engine/Twig/SlickTwigExtension.php +++ b/src/Slick/Template/Engine/Twig/SlickTwigExtension.php @@ -12,9 +12,11 @@ namespace Slick\Template\Engine\Twig; -use Slick\I18n\TranslateMethods; -use Slick\I18n\Translator; +use Twig_Extension; +use Twig_Environment; +use Twig_SimpleFilter; use Slick\Version\Version; +use Slick\I18n\TranslateMethods; use Zend\Http\PhpEnvironment\Request; /** @@ -23,7 +25,7 @@ * @package Slick\Template\Engine * @author Filipe Silva */ -class SlickTwigExtension extends \Twig_Extension +class SlickTwigExtension extends Twig_Extension { /** @@ -107,6 +109,27 @@ public function getGlobals() ]; } + /** + * Returns a list of filters. + * + * @return array + */ + public function getFilters() + { + return [ + new Twig_SimpleFilter( + 'truncate', + '\Slick\Template\Engine\Twig\truncateFilter', + ['needs_environment' => true] + ), + new Twig_SimpleFilter( + 'wordwrap', + '\Slick\Template\Engine\Twig\wordwrapFilter', + ['needs_environment' => true] + ), + ]; + } + /** * Lazy load of the HTTP response object * @@ -127,6 +150,86 @@ protected function addLinkRef($name, $folder = null) $path = "{$base}/{$folder}{$name}"; return $path; } +} + +if (function_exists('mb_get_info')) { + function truncateFilter( + Twig_Environment $env, $value, $length = 30, $preserve = false, + $separator = '...') + { + if (mb_strlen($value, $env->getCharset()) > $length) { + if ($preserve) { + // If breakpoint is on the last word, return the + // value without separator. + if ( + false === ( + $breakpoint = mb_strpos( + $value, + ' ', + $length, + $env->getCharset() + ) + ) + ) { + return $value; + } + + $length = $breakpoint; + } + + return rtrim( + mb_substr($value, 0, $length, $env->getCharset()) + ) . $separator; + } + + return $value; + } + + function wordwrapFilter( + Twig_Environment $env, $value, $length = 80, $separator = "\n", + $preserve = false) + { + $sentences = array(); + + $previous = mb_regex_encoding(); + mb_regex_encoding($env->getCharset()); + + $pieces = mb_split($separator, $value); + mb_regex_encoding($previous); + + foreach ($pieces as $piece) { + while(!$preserve && mb_strlen($piece, $env->getCharset()) > $length) { + $sentences[] = mb_substr($piece, 0, $length, $env->getCharset()); + $piece = mb_substr($piece, $length, 2048, $env->getCharset()); + } + + $sentences[] = $piece; + } + + return implode($separator, $sentences); + } +} else { + function truncateFilter( + Twig_Environment $env, $value, $length = 30, $preserve = false, + $separator = '...') + { + if (strlen($value) > $length) { + if ($preserve) { + if (false !== ($breakpoint = strpos($value, ' ', $length))) { + $length = $breakpoint; + } + } + return rtrim(substr($value, 0, $length)) . $separator; + } -} \ No newline at end of file + return $value; + } + + function wordwrapFilter( + Twig_Environment $env, $value, $length = 80, $separator = "\n", + $preserve = false) + { + return wordwrap($value, $length, $separator, !$preserve); + } +} diff --git a/tests/unit/Mvc/ScaffoldTest.php b/tests/unit/Mvc/ScaffoldTest.php new file mode 100644 index 0000000..47687f2 --- /dev/null +++ b/tests/unit/Mvc/ScaffoldTest.php @@ -0,0 +1,94 @@ + + * @copyright 2014 Filipe Silva + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @since Version 1.1.0 + */ + +namespace Mvc; + +use CodeGuy; +use Codeception\Util\Stub; +use Codeception\TestCase\Test; +use Slick\Mvc\Controller; +use Slick\Mvc\Dispatcher; +use Slick\Mvc\Scaffold; +use Zend\Http\PhpEnvironment\Request; +use Zend\Http\PhpEnvironment\Response; + +/** + * Scaffold controller test case + * + * @package Test\Mvc + * @author Filipe Silva + */ +class ScaffoldTest extends Test +{ + /** + * @var CodeGuy + */ + protected $codeGuy; + + /** + * Trying to create a scaffold controller + * @test + */ + public function createScaffoldController() + { + $routeInfo = Stub::make( + 'Slick\Mvc\Router\RouteInfo', + [ + 'getController' => function() { + return 'Mvc\MyScaffoldController'; + } + ] + ); + $application = Stub::make( + 'Slick\Mvc\Application', + [ + 'getRequest' => function() {return new Request(); }, + 'getResponse' => function() {return new Response(); } + ] + ); + $dispatcher = new Dispatcher( + [ + 'routeInfo' => $routeInfo, + 'application' => $application + ] + ); + /** @var Scaffold $scaffold */ + $scaffold = $dispatcher->getController(); + $this->assertInstanceOf('Slick\Mvc\Scaffold', $scaffold); + $this->assertInstanceOf( + 'Mvc\MyScaffoldController', + $scaffold->getController() + ); + + $this->assertEquals('Models\Myscaffoldcontroller', $scaffold->modelName); + $this->assertEquals('myscaffoldcontrollers', $scaffold->get('modelPlural')); + $this->assertEquals('myscaffoldcontroller', $scaffold->get('modelSingular')); + $scaffold->setModelName("Models\\User"); + $this->assertEquals('users', $scaffold->get('modelPlural')); + $this->assertEquals('user', $scaffold->get('modelSingular')); + } + +} + +/** + * Mock controller for test + * + * @package Mvc + */ +class MyScaffoldController extends Controller +{ + /** + * @readwrite + * @var bool + */ + protected $_scaffold = true; +}