I'm trying to write a route for an level N category depth. So an usual category URL would look like this:
http://website/my-category/my-subcategory/my-subcategory-level3/my-subcategory-level4
It has an unknown depth and my route has to match all possible levels. I made a route for this, but I can't get all the params from my controller.
$routeCategory = new Zend_Controller_Router_Route_Regex(
'(([a-z0-9-]+)/?){1,}',
array(
'module' => 'default',
'controller' => 'index',
'action' => 'index'
),
array( 1 => 'path'),
'%s'
);
$router->addRoute('category', $routeCategory);
I can't seem to find a way to send the route matched params to the controller. If you have a better solution, I'm open to suggestions!
I found a solution that I think fits my needs. I'll post it here for people who will end up in the same thing I got into.
Problem:
need custom route for level N categories like category/subcategory/subsubcategory/...
custom route for N categories + object like category/subcategory/../page.html
preserve Zend Framework's default routing (for other modules, admin for example)
URL assembling with URL helper
Solution:
create custom route class (I used Zend_Controller_Router_Route_Regex as a starting point so I can benefit from the assemble() method)
Actual code:
<?php
class App_Controller_Router_Route_Category extends Zend_Controller_Router_Route_Regex
{
public function match($path, $partial = false)
{
if (!$partial) {
$path = trim(urldecode($path), '/');
}
$values = explode('/', $path);
$res = (count($values) > 0) ? 1 : 0;
if ($res === 0) {
return false;
}
/**
* Check if first param is an actual module
* If it's a module, let the default routing take place
*/
$modules = array();
$frontController = Zend_Controller_Front::getInstance();
foreach ($frontController->getControllerDirectory() as $module => $path) {
array_push($modules, $module);
}
if(in_array($values[0], $modules)) {
return false;
}
if ($partial) {
$this->setMatchedPath($values[0]);
}
$myValues = array();
$myValues['cmsCategory'] = array();
// array_filter_key()? Why isn't this in a standard PHP function set yet? :)
foreach ($values as $i => $value) {
if (!is_int($i)) {
unset($values[$i]);
} else {
if(preg_match('/.html/', $value)) {
$myValues['cmsObject'] = $value;
} else {
array_push($myValues['cmsCategory'], $value);
}
}
}
$values = $myValues;
$this->_values = $values;
$values = $this->_getMappedValues($values);
$defaults = $this->_getMappedValues($this->_defaults, false, true);
$return = $values + $defaults;
return $return;
}
public function assemble($data = array(), $reset = false, $encode = false, $partial = false)
{
if ($this->_reverse === null) {
require_once 'Zend/Controller/Router/Exception.php';
throw new Zend_Controller_Router_Exception('Cannot assemble. Reversed route is not specified.');
}
$defaultValuesMapped = $this->_getMappedValues($this->_defaults, true, false);
$matchedValuesMapped = $this->_getMappedValues($this->_values, true, false);
$dataValuesMapped = $this->_getMappedValues($data, true, false);
// handle resets, if so requested (By null value) to do so
if (($resetKeys = array_search(null, $dataValuesMapped, true)) !== false) {
foreach ((array) $resetKeys as $resetKey) {
if (isset($matchedValuesMapped[$resetKey])) {
unset($matchedValuesMapped[$resetKey]);
unset($dataValuesMapped[$resetKey]);
}
}
}
// merge all the data together, first defaults, then values matched, then supplied
$mergedData = $defaultValuesMapped;
$mergedData = $this->_arrayMergeNumericKeys($mergedData, $matchedValuesMapped);
$mergedData = $this->_arrayMergeNumericKeys($mergedData, $dataValuesMapped);
/**
* Default Zend_Controller_Router_Route_Regex foreach insufficient
* I need to urlencode values if I bump into an array
*/
if ($encode) {
foreach ($mergedData as $key => &$value) {
if(is_array($value)) {
foreach($value as $myKey => &$myValue) {
$myValue = urlencode($myValue);
}
} else {
$value = urlencode($value);
}
}
}
ksort($mergedData);
$reverse = array();
for($i = 0; $i < count($mergedData['cmsCategory']); $i++) {
array_push($reverse, "%s");
}
if(!empty($mergedData['cmsObject'])) {
array_push($reverse, "%s");
$mergedData['cmsCategory'][] = $mergedData['cmsObject'];
}
$reverse = implode("/", $reverse);
$return = #vsprintf($reverse, $mergedData['cmsCategory']);
if ($return === false) {
require_once 'Zend/Controller/Router/Exception.php';
throw new Zend_Controller_Router_Exception('Cannot assemble. Too few arguments?');
}
return $return;
}
}
Usage:
Route:
$routeCategory = new App_Controller_Router_Route_Category(
'',
array(
'module' => 'default',
'controller' => 'index',
'action' => 'index'
),
array(),
'%s'
);
$router->addRoute('category', $routeCategory);
URL Helper:
echo "<br>Url: " . $this->_helper->url->url(array(
'module' => 'default',
'controller' => 'index',
'action' => 'index',
'cmsCategory' => array(
'first-category',
'subcategory',
'subsubcategory')
), 'category');
Sample output in controller with getAllParams()
["cmsCategory"]=>
array(3) {
[0]=>
string(15) "first-category"
[1]=>
string(16) "subcategory"
[2]=>
string(17) "subsubcategory"
}
["cmsObject"]=>
string(15) "my-page.html"
["module"]=>
string(7) "default"
["controller"]=>
string(5) "index"
["action"]=>
string(5) "index"
Note the cmsObject is set only when the URL contains something like category/subcategory/subsubcategory/my-page.html
I've done it without routes... I've routed only the first parameter and then route the others getting all the params inside the controller
Route:
resources.router.routes.catalog-display.route = /catalog/item/:id
resources.router.routes.catalog-display.defaults.module = catalog
resources.router.routes.catalog-display.defaults.controller = item
resources.router.routes.catalog-display.defaults.action = display
as example:
I use this for the catalog, then into the itemController into the displayAction I check for $this->getRequest()->getParams(), the point is that you can (but I think that you know it) read all the params passed in the way key/value, as example: "site.com/catalog/item/15/kind/hat/color/red/size/M" will produce an array as: $params['controller'=>'catalog','action'=>'display','id'=>'15','kind'=>'hat','color'=>'red','size'=>'M'];
For anyone stumbling across this question using Zend Framework 2 or Zend Framework 3, there is a regex route type which can (and probably should) be used for the OP's route in which there is an unknown number of parameters dependent on the number of child categories. To use, add the following line to the top of your router config:
use Zend\Router\Http\Regex;
Then, you can use a route such as the following to match an unknown number of categories:
'categories' => [
'type' => Regex::class,
'options' => [
'regex' => '/categories(?<sequence>(/[\w\-]+)+)',
'defaults' => [
'controller' => ApplicationController\Categories::class,
'action' => 'view',
],
'spec' => '%sequence',
],
],
The above route will match the following routes:
/categories/parent-cat
/categories/parent-cat/child-cat
/categories/parent-cat/sub-child-cat
/categories/parent-cat/sub-sub-child-cat
/categories/parent-cat-2
/categories/parent-cat-2/child-cat
... and so on. The sequence of categories is passed to the controller in the sequence parameter. You can process this parameter as desired in your controller.
Related
I'm migrating from zend framework 1 to 3 , and I have a function that returns twig template but I don't know what should I use to render view twig template on zf3
How to:
use class of viewer
set my template path
set array to render it in template
return template
code:
protected function convertItemList($aItemList)
{
$aSet = [];
//$config['template_paths'] = [APPLICATION_PATH . '/../library/Core/Backend/SRO/Views/'];
//$oView = new Core_Twig_View($config);
if (!$aItemList) {
return [];
}
foreach ($aItemList as $iKey => $aCurItem) {
$aSpecialInfo = [];
$aInfo = $aCurItem;
$aInfo['info'] = $this->getItemInfo($aCurItem);
$aInfo['blues'] = $this->getBluesStats($aCurItem, $aSpecialInfo);
$aInfo['whitestats'] = $this->getWhiteStats($aCurItem, $aSpecialInfo);
//$oView->assign('aItem', $aInfo);
$i = isset($aCurItem['Slot']) ? $aCurItem['Slot'] : $aCurItem['ID64'];
if ($aCurItem['MaxStack'] > 1) {
$aSet[$i]['amount'] = $aCurItem['Data'];
}
$aSet[$i]['TypeID2'] = $aInfo['TypeID2'];
$aSet[$i]['OptLevel'] = $aInfo['OptLevel'];
$aSet[$i]['RefItemID'] = !isset($aCurItem['RefItemID']) ? 0 : $aCurItem['RefItemID'];
$aSet[$i]['special'] = isset($aInfo['info']['sox']) && $aInfo['info']['sox'] ? true : false;
$aSet[$i]['ItemID'] = $aCurItem['ID64'];
$aSet[$i]['ItemName'] = $aInfo['info']['WebName'];
$aSet[$i]['imgpath'] = $this->getItemIcon($aCurItem['AssocFileIcon128']);
//$aSet[$i]['data'] = $oView->render('itemData.twig');
}
return $aSet;
}
I use this module https://github.com/OxCom/zf3-twig.
You can install it by github instructions and add this parameter to zf3 configuration array:
'service_manager' => array(
'factories' => array(
...
'TwigStrategy' => \ZendTwig\Service\TwigStrategyFactory::class,
...
),
)
1) After this you can use Twig in some action of some controller by this code:
function someAction(){
...
$viewModel = new ZendTwig\View\TwigModel(['foo'=>'bar']);
return $viewModel;
}
2) To set other template:
function someAction(){
$viewModel = new ZendTwig\View\TwigModel(['foo'=>'bar']);
$viewModel->setTemplate('application/controller/name'); //set path here
return $viewModel;
}
3) You can to set array variables by TwigModel "__construct" parameter:
function someAction(){
$viewModel = new ZendTwig\View\TwigModel($someVariablesArray);
$viewModel->setTemplate('application/controller/name'); //set path here
return $viewModel;
}
4) If you need to return html code, you need to do something:
Add in services config one more param:
'service_manager' => array(
'factories' => array(
...
'TwigStrategy' => \ZendTwig\Service\TwigStrategyFactory::class,
'TwigRenderer' => \ZendTwig\Service\TwigRendererFactory::class,
...
),
)
Add TwigRenderer service in your controller factory:
class YourControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new YourController($twigRenderer);
}
}
and get twigRenderer in your Controller:
private $twigRenderer;
public function __construct($twigRenderer)
{
$this->twigRenderer = $twigRenderer;
}
After this get html:
function someAction(){
$viewModel = new ZendTwig\View\TwigModel(['foo'=>'bar']);
$viewModel->setTemplate('mails/order/order_in_process');
$html = $this->twigRenderer->render($viewModel);
return $html;
}
Sorry for my english!
I have two servers, the first works using REST and other Webs pages that are built using MVC.
I usually always run REST commands in the Controller layer, however I am with a doubt, assuming that my project (using MVC) does not use database and I'm using Controllers only to send commands to the webservice to send and receive information.
The REST this case would be the like Model?
In this case I should call the Rest within the Controllers and not create Models. eg .:
public function createProductAction() {
$rest = Rest('ws.example.com', 'PUT /items/', array(
'price' => $price,
'description' => $descricao
));
if ($response->status === 200) {
View::show('success.tpl');
} else {
View::show('error.tpl', $rest->error());
}
}
public function viewProductAction() {
$rest = Rest('ws.example.com', 'GET /items/{id}', array(
'id' => $_GET['id']
));
$response = json_decode($rest->getRespose());
if ($response->status() === 200) {
View::show('product.tpl', $response);
} else {
View::show('error.tpl', $rest->error());
}
}
or
I would have to create Models to make the calls to REST?
For example:
class ProductsModel
{
public function putItem($preco, $descricao)
{
$rest = Rest('ws.example.com', 'PUT /items/', array(
'price' => $price,
'description' => $descricao
));
//If status=200 new product is added
return $response->status() === 200;
}
public function deleteItem($id)
{
$rest = Rest('ws.example.com', 'DELETE /items/{id}', array(
'id' => $id
));
//If status=200 product is deleted
return $rest->status() === 200;
}
public function getItem($id)
{
$rest = Rest('ws.example.com', 'GET /items/{id}', array(
'id' => $id
));
if ($rest->status() === 200) {
//If status=200 return data
return json_decode($rest->getRespose());
}
return NULL;
}
}
How should I proceed?
i want to test a controller function getStructuredChartData which takes $chartData as a parameter
function getStructuredChartData($chartData = null) {
$structuredChartData = array();
$structuredChartData['Create'] = array();
$structuredChartData['Create']['type'] = 'column';
$structuredChartData['Create']['exportingEnabled'] = TRUE;
if($chartData == null) {
$structuredChartData['null'] = TRUE;
} else {
$structuredChartData['ChartParams'] = array();
$structuredChartData['ChartParams']['renderTo'] = 'columnwrapper';
....
....
....
}
}
and to test this the code i have written in testcase controller is as follows
public function testTrendsReportWithAjaxRequest() {
$chartData = array();
$from_date = new DateTime("2014-07-01");
$to_date = new DateTime("2014-07-31");
$chartData['StartDate'] = $from_date;
$chartData['EndDate'] = $to_date;
$chartData['View'] = "Daily";
$chartData['Data'][(int) 0]['Project']['name'] = 'Test Project #1';
$chartData['Data'][(int) 0][(int) 0] = array('reviewed' => '1', 'modified' => '2014-07-16');
debug($chartData);
// Invoke the index action.
$result = $this->testAction(
'/reports/getStructuredChartData',
array('data' => $chartData)
);
debug($result);
$this->assertNotEmpty($result);
}
now my concern is that how to pass $chartData to controller function in testCase.
Currently in Controller function $chartData occurs as NULL and the if condition
if($chartData == null) {
$structuredChartData['null'] = TRUE;
}
gets executed. moreover i would like else condition
else {
$structuredChartData['ChartParams'] = array();
$structuredChartData['ChartParams']['renderTo'] = 'columnwrapper';
....
....
....
}
to be executed.
From CakePHP testing documentation
By supplying the data key, the request made to the controller will be POST. By default all requests will be POST requests. You can simulate a GET request by setting the method key:
You have to add get as method:
$result = $this->testAction(
'/reports/getStructuredChartData',
array('data' => $chartData, 'method' => 'get')
);
If you have your routes properly configured you can just pass the full url:
$result = $this->testAction(
'/reports/getStructuredChartData/test'
);
Again from CakePHP docs
// routes.php
Router::connect(
'/reports/getStructuredData/:chartData',
array('controller' => 'reports', 'action' => 'getStructuredData'),
array(
'pass' => array('chartData'),
)
);
To pass $chartData as argument in TestCaseController what i did is ::
$this->controller = new ReportsController();
$result = $this->controller->getStructuredChartData($chartData);
I am looking for a way to access and change the DATABASE_CONFIG variables, based on user input. Using CakePHP I created a custom datasource, based on the one provided in the docs, to access an external API. The API returns a JSON string containing the 12 most recent objects. I need to be able to change the page number in the API request to get the next 12 results, as well as accept a free text query entered by the user.
app/Config/Database.php
class DATABASE_CONFIG {
public $behance = array(
'datasource' => 'BehanceDatasource',
'api_key' => '123456789',
'page' => '1',
'text_query' => 'foo'
);
}
app/Model/Datasource/BehanceDataSource.php
App::uses('HttpSocket', 'Network/Http');
class BehanceDatasource extends DataSource {
public $description = 'Beehance datasource';
public $config = array(
'api_key' => '',
'page' => '',
'text_query' => ''
);
public function __construct($config) {
parent::__construct($config);
$this->Http = new HttpSocket();
}
public function listSources($data = null) {
return null;
}
public function describe($model) {
return $this->_schema;
}
public function calculate(Model $model, $func, $params = array()) {
return 'COUNT';
}
public function read(Model $model, $queryData = array(), $recursive = null) {
if ($queryData['fields'] === 'COUNT') {
return array(array(array('count' => 1)));
}
$queryData['conditions']['api_key'] = $this->config['api_key'];
$queryData['conditions']['page'] = $this->config['page'];
$queryData['conditions']['page'] = $this->config['text_query'];
$json = $this->Http->get('http://www.behance.net/v2/projects', $queryData['conditions']);
$res = json_decode($json, true);
if (is_null($res)) {
$error = json_last_error();
throw new CakeException($error);
}
return array($model->alias => $res);
}
}
Is there anyway to access and change the $behance array, or is there another way to go about accessing an external API with cakePHP that I am totally missing?
Playing around with twig templates (not a part of Symfony, in CodeIgniter) and it doesn't look like I can get a global sandbox to work correctly. I'm pretty sure I'm just doing something stupid, but I really can't see it.
Functions to make twig work:
public function twig_setup($configs = NULL)
{
Twig_Autoloader::register();
$env = array(
'cache' => config_item('cache_dir'),
'strict_variables' => TRUE,
'auto_reload' => TRUE,
'extension' => 'php',
'filesystem' => NULL
);
if (!is_null($configs))
{
$env = array_merge($env, $configs);
}
$this->set_extension($env['extension']);
if (is_null($env['filesystem']))
{
$env['filesystem'] = VIEWPATH;
}
else
{
$env['filesystem'] = VIEWPATH .'/'. ltrim($env['filesystem'],'/');
}
$this->set_filesystem($env['filesystem']);
// These two things should not get set to the environment
unset($env['extension']);
unset($env['filesystem']);
$this->set_environment($env);
}
public function set_sandbox($tags, $filters, $methods, $properties, $functions, $is_global = TRUE)
{
$user_policy = new Twig_Sandbox_SecurityPolicy($tags, $filters, $methods, $properties, $functions);
$user_sandbox = new Twig_Extension_Sandbox($user_policy, $is_global);
$this->twig->addExtension($user_sandbox);
}
public function disable_logic()
{
$tags = array('for', 'block', 'include');
$filters = array();
$methods = array();
$properties = array();
$functions = array('parent', 'block');
$this->set_sandbox($tags, $filters, $methods, $properties, $functions);
}
Usage:
$twig = new TwigThing();
$twig->twig_setup();
$twig->disable_logic();
Now, once I render a template, I should not be able to use something like raw or url_encode
{{ my_var|url_encode }}
That should kick out an error, or something, but it just encodes the var...wtf am I doing wrong here?