There's a few ways to implement such behavior in phalcon. Before anything, we need to understand how views and view helpers work in phalcon. And if you pay close attention, you'll notice, both .volt
and .phtml
have direct access to the DI
.
In volt, for example, you can access the flash
service, and output its messages by calling:
{{ flash.output() }}
which gets converted to the phtml: <?php echo $this->flash->output(); ?>
Thus my solution focuses on defining a new service in the DI which volt can access. In CakePHP, the syntax for postLink()
, looks something like: echo $this->Form->postLink()
while the function is actually defined in a class named FormHelper
. So my solution will do the same thing, define a class FormHelper
, then inject it into the view under the name Form
.
- Create an
app/helpers/
directory.
- Update your
app/config/config.php
file adding a reference to our new directory: 'helpersDir'=> APP_PATH . '/app/helpers/'
- Update your
app/config/loader.php
file adding $config->application->helpersDir
to the registered directories.
- Create a new file
app/helpers/FormHelper.php
- Copy-paste the following code into the file:
<?php
use Phalcon\Tag;
class FormHelper extends Tag
{
protected $_lastAction = '';
public function dottedNameToBracketNotation($name)
{
$parts=explode('.',$name);
$first = array_shift($parts);
$name=$first . ($parts ? '[' . implode('][', $parts) . ']' : '');
return $name;
}
protected function flatten(array $data, $separator = '.')
{
$result = [];
$stack = [];
$path = null;
reset($data);
while (!empty($data)) {
$key = key($data);
$element = $data[$key];
unset($data[$key]);
if (is_array($element) && !empty($element)) {
if (!empty($data)) {
$stack[] = [$data, $path];
}
$data = $element;
reset($data);
$path .= $key . $separator;
} else {
$result[$path . $key] = $element;
}
if (empty($data) && !empty($stack)) {
list($data, $path) = array_pop($stack);
reset($data);
}
}
return $result;
}
protected function _confirm($message, $okCode, $cancelCode = '', $options = [])
{
$message = json_encode($message);
$confirm = "if (confirm({$message})) { {$okCode} } {$cancelCode}";
if (isset($options['escape']) && $options['escape'] === false) {
$confirm = $this->h($confirm);
}
return $confirm;
}
public function h($text, $double = true, $charset = 'UTF-8')
{
return htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, $charset, $double);
}
protected function _lastAction($url)
{
$action = $url;//Router::url($url, true);
$query = parse_url($action, PHP_URL_QUERY);
$query = $query ? '?' . $query : '';
$this->_lastAction = parse_url($action, PHP_URL_PATH) . $query;
}
public function postLink($title, $url = null, array $options = [])
{
$out='';
$options += ['block' => null, 'confirm' => null];
$requestMethod = 'POST';
if (!empty($options['method'])) {
$requestMethod = strtoupper($options['method']);
unset($options['method']);
}
$confirmMessage = $options['confirm'];
unset($options['confirm']);
$formName = str_replace('.', '', uniqid('post_', true));
$formOptions = [
'name' => $formName,
'style' => 'display:none;',
'method' => 'post',
];
if (isset($options['target'])) {
$formOptions['target'] = $options['target'];
unset($options['target']);
}
$formOptions[0]=$url;
$out.=$this->form($formOptions);
$out .= $this->hiddenField(['_method','value' => $requestMethod]);
$fields = [];
if (isset($options['data']) && is_array($options['data'])) {
foreach ($this->flatten($options['data']) as $key => $value) {
$out .= $this->hiddenField([$this->dottedNameToBracketNotation($key),'value' => $value]);
}
unset($options['data']);
}
$out .= $this->endForm();
//This is currently unsupported
if ($options['block']) {
if ($options['block'] === true) {
$options['block'] = __FUNCTION__;
}
//$this->_View->append($options['block'], $out);
$out = '';
}
unset($options['block']);
$url = '#';
$onClick = 'document.' . $formName . '.submit();';
if ($confirmMessage) {
$options['onclick'] = $this->_confirm($confirmMessage, $onClick, '', $options);
} else {
$options['onclick'] = $onClick . ' ';
}
$options['onclick'] .= 'event.returnValue = false; return false;';
$options[0]=$url;
$options[1]=$title;
$options[2]=false;
$out .= $this->linkTo($options);
return $out;
}
}
- Edit your
app/config/services.php
file and add in:
$di->set('Form',function () {
return new FormHelper();
});
(you could make "Form" lowercase if you want, both work. I made it capital to closer resemble CakePHP's syntax. Do note that Volt is case sensitive when trying to access services but phtml will lowercase it.)
- Edit the template you want to test the code on, such as
app/views/index/test.volt
- Copy-paste the following code into there:
{{ Form.postLink(' Delete','',['confirm':'Are you sure you want to delete #4?','data':['a':['b','c']]]) }}
Alternatively for phtml, use:
<?php echo $this->form->postLink(' Delete', '', array('confirm' => 'Are you sure you want to delete #4?', 'data' => array('a' => array('b', 'c')))); ?>
Run it, and watch it work its magic, just render your index/test.volt template by visiting /index/test
in your address bar. (Make sure you defined such an action in your index controller)
In terms, of other solutions, you could also use $compiler->addFunction()
to make functions available to volt, one at time. The page in the manual gives the example of $compiler->addFunction('shuffle', 'str_shuffle');
. You can attempt to override the factoryDefault for "tag" in the DI, and use the helper we already defined which extends tag. So you'd just change it from "form" to "tag" like so: $di->set('tag',function () {return new FormHelper();});
but, as you can see, it won't make the function postLink()
available to volt as a function, you'll notice you still need to access it as tag.postLink()
. Rather, all the \Phalcon\Tag functions are actually hard-coded into the volt engine. You can see this clearly by viewing the zephir source code of the \Phalcon\Mvc\View\Engine\Volt\Compiler
class available over here. For your convenience, and in case the link ever gets broken, I have posted a snippet here which shows the "tag" functions in volt are actually hard-coded into it:
if method_exists(className, method) {
let arrayHelpers = this->_arrayHelpers;
if typeof arrayHelpers != "array" {
let arrayHelpers = [
"link_to": true,
"image": true,
"form": true,
"select": true,
"select_static": true,
"submit_button": true,
"radio_field": true,
"check_field": true,
"file_field": true,
"hidden_field": true,
"password_field": true,
"text_area": true,
"text_field": true,
"email_field": true,
"date_field": true,
"tel_field": true,
"numeric_field": true,
"image_input": true
];
let this->_arrayHelpers = arrayHelpers;
}
if isset arrayHelpers[name] {
return "$this->tag->" . method . "(array(" . arguments . "))";
}
return "$this->tag->" . method . "(" . arguments . ")";
}
So, if you'd like to "hack" in a few more methods by extending the \Phalcon\Tags
class, you're out of luck. However, as demonstrated on the volt documentation page, there exists the concept of registering custom extensions to work with volt. The documentation gives the example of: $compiler->addExtension(new PhpFunctionExtension());
Where the source of the class is:
<?php
class PhpFunctionExtension
{
/**
* This method is called on any attempt to compile a function call
*/
public function compileFunction($name, $arguments)
{
if (function_exists($name)) {
return $name . '('. $arguments . ')';
}
}
}
This would allow volt access to any function you'd like, without having to manually register every possible function you could possibly ever need. You can test this by trying to access str_shuffle
in volt, like we did before with $compiler->addFunction('shuffle', 'str_shuffle');
but this time without having to register it.
In terms of other solutions, you could also try to integrate CakePHP and PhalconPHP together, and attempt to call CakePHP's view helpers from PhalconPHP, but then you'd run into a problem of CakePHP not understanding your router setup you have configured in Phalcon. But, if you're determined, you could code all the routes and config for CakePHP and run it alongside PhalconPHP, but I'd highly discourage such a desperate workaround. And, finally, if you understand how the function works, and you barely use it, you could get away with just hard-coding the HTML in the first place. Honestly, CakePHP's logic doesn't look so sound to me in the first place because it has to corrupt your HTML document with a form inserted which can bother your layout. I think it would make more sense to generate a form dynamically with JavaScript, if we're using JavaScript already, and append it to the <body>
when the button is clicked, then submit the form we just created dynamically. But, you wanted a CakePHP implementation, so I coded it as close to the logic they used as possible. It's not perfect, in terms of supporting all their features, such as block
, but it should suit most of your needs.
I can always revise my implementation, but I think it demonstrates how to work with Phalcon pretty well for those migrating from CakePHP.