diff --git a/.gitignore b/.gitignore index ed9e4e552e6..790c55c2ea1 100644 --- a/.gitignore +++ b/.gitignore @@ -43,11 +43,11 @@ ext/run-tests.php lemon .libs/ -.temp/ autom4te.cache/ /vendor /ide/ +.zephir/ boxfile.yml composer.lock php_test_results_*.txt diff --git a/CHANGELOG-4.0.md b/CHANGELOG-4.0.md index 91545add933..360c351fb6d 100644 --- a/CHANGELOG-4.0.md +++ b/CHANGELOG-4.0.md @@ -12,7 +12,7 @@ - Added more column types for the Mysql adapter. The adapter supports `TYPE_BIGINTEGER`, `TYPE_BIT`, `TYPE_BLOB`, `TYPE_BOOLEAN`, `TYPE_CHAR`, `TYPE_DATE`, `TYPE_DATETIME`, `TYPE_DECIMAL`, `TYPE_DOUBLE`, `TYPE_ENUM`, `TYPE_FLOAT`, `TYPE_INTEGER`, `TYPE_JSON`, `TYPE_JSONB`, `TYPE_LONGBLOB`, `TYPE_LONGTEXT`, `TYPE_MEDIUMBLOB`, `TYPE_MEDIUMINTEGER`, `TYPE_MEDIUMTEXT`, `TYPE_SMALLINTEGER`, `TYPE_TEXT`, `TYPE_TIME`, `TYPE_TIMESTAMP`, `TYPE_TINYBLOB`, `TYPE_TINYINTEGER`, `TYPE_TINYTEXT`, `TYPE_VARCHAR` [#13151](https://github.com/phalcon/cphalcon/issues/13151), [#12223](https://github.com/phalcon/cphalcon/issues/12223), [#524](https://github.com/phalcon/cphalcon/issues/524), [#13225](https://github.com/phalcon/cphalcon/pull/13225) [@zGaron](https://github.com/zGaron), [#12523](https://github.com/phalcon/cphalcon/pull/12523) [@Studentsov](https://github.com/Studentsov), [#12471](https://github.com/phalcon/cphalcon/pull/12471) [@ruudboon](https://github.com/ruudboon) - Added `Phalcon\Acl\Adapter\Memory::addRole` support multiple inherited - Added `Phalcon\Tag::renderTitle()` that renders the title enclosed in `` tags. [#13547](https://github.com/phalcon/cphalcon/issues/13547) -- Added `hasHeader()` method to `Phalcon\Http\Response` to provide the ability to check if a header exists. [PR-12189](https://github.com/phalcon/cphalcon/pull/12189) +- Added `hasHeader()` method to `Phalcon\Http\Response` to provide the ability to check if a header exists. [#12189](https://github.com/phalcon/cphalcon/pull/12189) - Added global setting `orm.case_insensitive_column_map` to attempt to find value in the column map case-insensitively. Can be also enabled by setting `caseInsensitiveColumnMap` key in `\Phalcon\Mvc\Model::setup()`. [#11802](https://github.com/phalcon/cphalcon/pull/11802) - Added the ability to use FrontendInterface to serialize Model and ResultSet - Inject a `serializer` object which implements `FrontendInterface` in DI to use it. [#12808] (https://github.com/phalcon/cphalcon/pull/12888) - Added `Phalcon\Mvc\Model\Query\BuilderInterface::offset` [#13599](https://github.com/phalcon/cphalcon/pull/13599) @@ -22,22 +22,23 @@ - Added response handler to `Phalcon\Mvc\Micro`, `Phalcon\Mvc\Micro::setResponseHandler`, to allow use of a custom response handler. [#12452](https://github.com/phalcon/cphalcon/pull/12452) - Added two new events `response::beforeSendHeaders` and `response::afterSendHeaders` to `Phalcon\Http\Response` [#10689](https://github.com/phalcon/cphalcon/issue/10689) - Added a retainer for the current token to be used during the checkings, so when `Phalcon\Security::getToken` is called the token used for checkings don't change. [#12392](https://github.com/phalcon/cphalcon/issues/12392) +- Added `Phalcon\Html\Tag`, a component that creates HTML elements. It will replace `Phalcon\Tag` in a future version. This component does not use static method calls. [#12392](https://github.com/phalcon/cphalcon/issues/12392) ## Changed -- By configuring `prefix` and `statsKey` the `Phalcon\Cache\Backend\Redis::queryKeys` no longer returns prefixed keys, now it returns original keys without prefix. [PR-13456](https://github.com/phalcon/cphalcon/pull/13456) +- By configuring `prefix` and `statsKey` the `Phalcon\Cache\Backend\Redis::queryKeys` no longer returns prefixed keys, now it returns original keys without prefix. [#13656](https://github.com/phalcon/cphalcon/pull/13656) - Now Phalcon requires the [PSR PHP extension](https://github.com/jbboehr/php-psr) to be installed and enabled -- The `Phalcon\Mvc\Application`, `Phalcon\Mvc\Micro` and `Phalcon\Mvc\Router` now must have a URI to process [PR-12380](https://github.com/phalcon/cphalcon/pull/12380) -- Response headers and cookies are no longer prematurely sent [PR-12378](https://github.com/phalcon/cphalcon/pull/12378) +- The `Phalcon\Mvc\Application`, `Phalcon\Mvc\Micro` and `Phalcon\Mvc\Router` now must have a URI to process [#12380](https://github.com/phalcon/cphalcon/pull/12380) +- Response headers and cookies are no longer prematurely sent [#12378](https://github.com/phalcon/cphalcon/pull/12378) - You can no longer assign data to models whilst saving them [#12317](https://github.com/phalcon/cphalcon/issues/12317) - The `Phalcon\Mvc\Model\Manager::load` no longer reuses already initialized models [#12317](https://github.com/phalcon/cphalcon/issues/12317) - Changed `Phalcon\Db\Dialect\Postgresql::describeReferences` to generate correct SQL, added "on update" and "on delete" constraints - Changed catch `Exception` to `Throwable` [#12288](https://github.com/phalcon/cphalcon/issues/12288) -- Changed `Phalcon\Mvc\Model\Query\Builder::addFrom` to remove third parameter `$with` [PR-13109](https://github.com/phalcon/cphalcon/pull/13109) -- `Phalcon\Forms\Form::clear` will no longer call `Phalcon\Forms\Element::clear`, instead it will clear/set default value itself, and `Phalcon\Forms\Element::clear` will now call `Phalcon\Forms\Form::clear` if it's assigned to the form, otherwise it will just clear itself. [PR-13500](https://github.com/phalcon/cphalcon/pull/13500) -- `Phalcon\Forms\Form::getValue` will now also try to get the value by calling `Tag::getValue` or element's `getDefault` method before returning `null`, and `Phalcon\Forms\Element::getValue` calls `Tag::getDefault` only if it's not added to the form. [PR-13500](https://github.com/phalcon/cphalcon/pull/13500) +- Changed `Phalcon\Mvc\Model\Query\Builder::addFrom` to remove third parameter `$with` [#13109](https://github.com/phalcon/cphalcon/pull/13109) +- `Phalcon\Forms\Form::clear` will no longer call `Phalcon\Forms\Element::clear`, instead it will clear/set default value itself, and `Phalcon\Forms\Element::clear` will now call `Phalcon\Forms\Form::clear` if it's assigned to the form, otherwise it will just clear itself. [#13500](https://github.com/phalcon/cphalcon/pull/13500) +- `Phalcon\Forms\Form::getValue` will now also try to get the value by calling `Tag::getValue` or element's `getDefault` method before returning `null`, and `Phalcon\Forms\Element::getValue` calls `Tag::getDefault` only if it's not added to the form. [#13500](https://github.com/phalcon/cphalcon/pull/13500) - Changed `Phalcon\Mvc\Model` to use the `Phalcon\Messages\Message` object for its messages [#13114](https://github.com/phalcon/cphalcon/issues/13114) - Changed `Phalcon\Validation\*` to use the `Phalcon\Messages\Message` object for its messages [#13114](https://github.com/phalcon/cphalcon/issues/13114) -- Collections now use the Validation component [PR-12376](https://github.com/phalcon/cphalcon/pull/12376) +- Collections now use the Validation component [#12376](https://github.com/phalcon/cphalcon/pull/12376) - Made the `specialKey` (`_PHCR`) optional for the `Phalcon\Cache\Backend\Redis` adapter [#10905](https://github.com/phalcon/cphalcon/issues/10905), [#11608](https://github.com/phalcon/cphalcon/pull/11608) - Refactored `Phalcon\Db\Adapter\Pdo::query` to use PDO's prepare and execute. `Phalcon\Db\Adapter::fetchAll` to use PDO's fetchAll - Fixed `\Phalcon\Http\Response::setFileToSend` filename last much _ diff --git a/phalcon/html/exception.zep b/phalcon/html/exception.zep new file mode 100644 index 00000000000..9cf48a6c4c8 --- /dev/null +++ b/phalcon/html/exception.zep @@ -0,0 +1,22 @@ + +/** + * This file is part of the Phalcon Framework. + * + * (c) Phalcon Team <team@phalconphp.com> + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +namespace Phalcon\Html; + +/** + * Phalcon\Html\Tag\Exception + * + * Exceptions thrown in Phalcon\Html\Tag will use this class + * + */ +class Exception extends \Phalcon\Exception +{ + +} diff --git a/phalcon/html/tag.zep b/phalcon/html/tag.zep new file mode 100644 index 00000000000..854534603da --- /dev/null +++ b/phalcon/html/tag.zep @@ -0,0 +1,1779 @@ + +/** + * This file is part of the Phalcon Framework. + * + * (c) Phalcon Team <team@phalconphp.com> + * + * For the full copyright and license information, please view the LICENSE.txt + * file that was distributed with this source code. + */ + +namespace Phalcon\Html; + +use Phalcon\DiInterface; +use Phalcon\Di\InjectionAwareInterface; +use Phalcon\Escaper; +use Phalcon\EscaperInterface; +use Phalcon\Html\Exception; +use Phalcon\UrlInterface; + +/** + * Phalcon\Html\Tag + * + * Phalcon\Tag is designed to simplify building of HTML tags. It provides a set + * of helpers to dynamically generate HTML. + */ +class Tag implements InjectionAwareInterface +{ + /** + * @var <DiInterface> + */ + protected container; + + /** + * @var array + */ + private append = []; + + /** + * @var int + */ + private docType = 5; // HTML5 + + /** + * @var <EscaperInterface> + */ + private escaper; + + /** + * @var array + */ + private prepend = []; + + /** + * @var string + */ + private separator = ""; + + /** + * @var string + */ + private title = ""; + + /** + * @var array + */ + private values = []; + + /** + * @var <UrlInterface> + */ + private url; + + /** + * Constants + */ + const HTML32 = 1; + const HTML401_STRICT = 2; + const HTML401_TRANSITIONAL = 3; + const HTML401_FRAMESET = 4; + const HTML5 = 5; + const XHTML10_STRICT = 6; + const XHTML10_TRANSITIONAL = 7; + const XHTML10_FRAMESET = 8; + const XHTML11 = 9; + const XHTML20 = 10; + const XHTML5 = 11; + + /** + * Appends a text to current document title + */ + public function appendTitle(array title) -> <Tag> + { + let this->append = title; + + return this; + } + + /** + * Builds a HTML input[type="button"] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->button('Click Me') + * </code> + * + * Volt syntax: + * <code> + * {{ button('Click Me) }} + * </code> + */ + public function button(string! name, array parameters = []) -> string + { + return this->renderInput("button", name, parameters); + } + + /** + * Resets the request and internal values to avoid those fields will have + * any default value. + */ + public function clear() -> void + { + let this->append = [], + this->docType = self::HTML5, + this->prepend = [], + this->separator = "", + this->title = "", + this->values = []; + } + + /** + * Builds a HTML tag + * + * Parameters + * `onlyStart` Only process the start of th element + * `selfClose` It is a self close element + * `useEol` Append PHP_EOL at the end + * + */ + public function element(string! tag, array parameters = []) -> string + { + var onlyStart, output, selfClose, useEol; + + let useEol = this->arrayGetDefault("useEol", parameters, false), + onlyStart = this->arrayGetDefault("onlyStart", parameters, false), + selfClose = this->arrayGetDefault("selfClose", parameters, false); + + /** + * Unset options for this control + */ + unset parameters["onlyStart"]; + unset parameters["selfClose"]; + unset parameters["useEol"]; + + let output = this->renderAttributes("<" . tag, parameters); + + if this->docType > self::HTML5 { + if selfClose { + let output .= " />"; + } else { + let output .= ">"; + } + } else { + if onlyStart { + let output .= ">"; + } else { + let output .= "></" . tag . ">"; + } + } + + if useEol { + let output .= PHP_EOL; + } + + return output; + } + + /** + * Builds the closing tag of an html element + * + * Parameters + * `useEol` Append PHP_EOL at the end + * + * <code> + * use Phalcon\Html\Tag; + * + * $tab = new Tag(); + * + * echo $tag->elementClose( + * [ + * 'name' => 'aside', + * ] + * ); // </aside> + * + * echo $tag->elementClose( + * [ + * 'name' => 'aside', + * 'useEol' => true, + * ] + * ); // '</aside>' . PHP_EOL + * </code> + * + */ + public function elementClose(string! tag, array parameters = []) -> string + { + var useEol = false; + + let useEol = this->arrayGetDefault("useEol", parameters, false); + + if useEol { + return "</" . tag . ">" . PHP_EOL; + } + return "</" . tag . ">"; + + } + + /** + * Returns the closing tag of a form element + */ + public function endForm(bool eol = true) -> string + { + if eol { + return "</form>" . PHP_EOL; + } else { + return "</form>"; + } + } + + /** + * Builds a HTML FORM tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tab = new Tag(); + * + * echo $tag->form('posts/save'); + * + * echo $tag->form( + * 'posts/save', + * [ + * "method" => "post", + * ] + * ); + * </code> + * + * Volt syntax: + * <code> + * {{ form('posts/save') }} + * {{ form('posts/save', ['method': 'post') }} + * </code> + */ + public function form(string action, array parameters = []) -> string + { + var output, params, service; + + let service = this->getService("url"); + + let parameters["method"] = this->arrayGetDefault("method", parameters, "post"), + parameters["action"] = service->get(action); + + /** + * Check for extra parameters + */ + if fetch params, parameters["parameters"] { + let parameters["action"] .= "?" . params; + unset parameters["parameters"]; + } + + let output = this->renderAttributes("<form", parameters) . ">"; + + return output; + + } + + /** + * Converts text to URL-friendly strings + * + * Parameters + * `text` The text to be processed + * `separator` Separator to use (default '-') + * `lowercase` Convert to lowercase + * `replace` + * + * <code> + * use Phalcon\Html\Tag; + * + * $tab = new Tag(); + * + * echo $tag->friendlyTitle( + * [ + * 'text' => 'These are big important news', + * 'separator' => '-', + * ] + * ); + * </code> + * + * Volt Syntax: + * <code> + * {{ friendly_title(['text': 'These are big important news', 'separator': '-']) }} + * </code> + */ + public function friendlyTitle(string! text, array parameters = []) -> string + { + var count, from, locale, lowercase, replace, separator, to, output; + + if extension_loaded("iconv") { + /** + * Save the old locale and set the new locale to UTF-8 + */ + let locale = setlocale(LC_ALL, "en_US.UTF-8"), + text = iconv("UTF-8", "ASCII//TRANSLIT", text); + } + + let lowercase = this->arrayGetDefault("lowercase", parameters, true), + replace = this->arrayGetDefault("replace", parameters, []), + separator = this->arrayGetDefault("separator", parameters, "-"); + + if !empty replace { + if typeof replace !== "array" && typeof replace !== "string"{ + throw new Exception("Parameter replace must be an array or a string"); + } + + if typeof replace === "string" { + let from = [replace]; + } else { + let from = replace; + } + + let count = count(from), + to = array_fill(0, count - 1, " "), + text = str_replace(from, to, text); + } + + let output = preg_replace("/[^a-zA-Z0-9\\/_|+ -]/", "", text); + if lowercase { + let output = strtolower(output); + } + + let output = preg_replace("/[\\/_|+ -]+/", separator, output), + output = trim(output, separator); + + if extension_loaded("iconv") { + /** + * Revert back to the old locale + */ + setlocale(LC_ALL, locale); + } + + return output; + } + + /** + * Returns the internal dependency injector + */ + public function getDI() -> <DiInterface> + { + return this->container; + } + + /** + * Get the document type declaration of content. If the docType has not + * been set properly, XHTML5 is returned + */ + public function getDocType() -> string + { + switch this->docType + { + case 1: + return "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 3.2 Final//EN\">" . PHP_EOL; + /* no break */ + + case 2: + return "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\"" . PHP_EOL . + "\t\"http://www.w3.org/TR/html4/strict.dtd\">" . PHP_EOL; + /* no break */ + + case 3: + return "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"" . PHP_EOL . + "\t\"http://www.w3.org/TR/html4/loose.dtd\">" . PHP_EOL; + /* no break */ + + case 4: + return "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01 Frameset//EN\"" . PHP_EOL . + "\t\"http://www.w3.org/TR/html4/frameset.dtd\">" . PHP_EOL; + /* no break */ + + case 6: + return "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"" . PHP_EOL . + "\t\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" . PHP_EOL; + /* no break */ + + case 7: + return "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"" . PHP_EOL . + "\t\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">" . PHP_EOL; + /* no break */ + + case 8: + return "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\"" . PHP_EOL . + "\t\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">" . PHP_EOL; + /* no break */ + + case 9: + return "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.1//EN\"" . PHP_EOL . + "\t\"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd\">" . PHP_EOL; + /* no break */ + + case 10: + return "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 2.0//EN\"" . PHP_EOL . + "\t\"http://www.w3.org/MarkUp/DTD/xhtml2.dtd\">" . PHP_EOL; + /* no break */ + } + + return "<!DOCTYPE html>" . PHP_EOL; + } + + /** + * Gets the current document title. The title will be automatically escaped. + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * $tag + * ->prependTitle('Hello') + * ->setTitle('World') + * ->appendTitle('from Phalcon'); + * + * echo $tag->getTitle(); // Hello World from Phalcon + * echo $tag->getTitle(false); // World from Phalcon + * echo $tag->getTitle(true, false); // Hello World + * echo $tag->getTitle(false, false); // World + * </code> + * + * Volt syntax: + * <code> + * {{ get_title() }} + * </code> + */ + public function getTitle(bool prepend = true, bool append = true) -> string + { + var item, items, output, title, appendTitle, prependTitle, separator, escaper; + + let escaper = this->getService("escaper"), + items = [], + output = "", + title = escaper->escapeHtml(this->title), + separator = escaper->escapeHtml(this->separator); + + if prepend { + let prependTitle = this->prepend; + + if !empty prependTitle { + var prependArray = array_reverse(prependTitle); + for item in prependArray { + let items[] = escaper->escapeHtml(item); + } + } + } + + if !empty title { + let items[] = title; + } + + if append { + let appendTitle = this->append; + + if !empty appendTitle { + for item in appendTitle { + let items[] = escaper->escapeHtml(item); + } + } + } + + if !empty items { + let output = implode(separator, items); + } + + return output; + } + + /** + * Gets the current document title separator + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->getTitleSeparator(); + * </code> + * + * Volt syntax: + * <code> + * {{ get_title_separator() }} + * </code> + */ + public function getTitleSeparator() -> string + { + return this->separator; + } + + /** + * Every helper calls this function to check whether a component has a predefined + * value using `setAttribute` or value from $_POST + */ + public function getValue(string name, array parameters = []) -> var | null + { + var value; + + if !fetch value, parameters["value"] { + /** + * Check if there is a predefined value for it + */ + if !fetch value, this->values[name] { + /** + * Check if there is a post value for the item + */ + if !fetch value, _POST[name] { + return null; + } + } + } + + return value; + } + + /** + * Check if a helper has a default value set using `setAttribute()` or + * value from $_POST + */ + public function hasValue(string name) -> bool + { + /** + * Check if there is a predefined or a POST value for it + */ + return isset this->values[name] || isset _POST[name]; + } + + /** + * Builds HTML IMG tags + * + * Parameters + * `local` Local resource or not (default `true`) + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->image('img/bg.png'); + * + * echo $tag->image( + * 'img/photo.jpg', + * [ + * 'alt' => 'Some Photo', + * ] + * ); + * + * echo $tag->image( + * 'http://static.mywebsite.com/img/bg.png', + * [ + * 'local' => false, + * ] + * ); + * </code> + * + * Volt Syntax: + * <code> + * {{ image('img/bg.png') }} + * {{ image('img/photo.jpg', ['alt': 'Some Photo') }} + * {{ image('http://static.mywebsite.com/img/bg.png', ['local': false]) }} + * </code> + */ + public function image(string url = "", array parameters = []) -> string + { + var local, service, src, output; + + let local = this->arrayGetDefault("local", parameters, true), + src = this->arrayGetDefault("src", parameters, url); + + /** + * Use the "url" service if the URI is local + */ + if local { + let service = this->getService("url"), + src = service->getStatic(url); + } + + unset parameters["local"]; + + let parameters["src"] = src; + + let output = this->renderAttributes("<img", parameters) . this->renderCloseTag(); + + return output; + } + + /** + * Builds a HTML input[type="check"] tag + * + *<code> + * echo $tag->inputCheckbox( + * [ + * 'name' => 'terms, + * 'value' => 'Y', + * ] + * ); + *</code> + * + * Volt syntax: + *<code> + * {{ input_checkbox(['name': 'terms, 'value': 'Y']) }} + *</code> + * + * @param array parameters + */ + public function inputCheckbox(string! name, array parameters = []) -> string + { + return this->renderInputChecked("checkbox", name, parameters); + } + + /** + * Builds a HTML input[type='color'] tag + */ + public function inputColor(string! name, array parameters = []) -> string + { + return this->renderInput("color", name, parameters); + } + + /** + * Builds a HTML input[type='date'] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->inputDate( + * [ + * 'name' => 'born', + * 'value' => '14-12-1980', + * ] + * ); + * </code> + * + * Volt syntax: + * <code> + * {{ input_date(['name':'born', 'value':'14-12-1980']) }} + * </code> + */ + public function inputDate(string! name, array parameters = []) -> string + { + return this->renderInput("date", name, parameters); + } + + /** + * Builds a HTML input[type='datetime'] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->inputDateTime( + * [ + * 'name' => 'born', + * 'value' => '14-12-1980', + * ] + * ); + * </code> + * + * Volt syntax: + * <code> + * {{ input_date_time(['name':'born', 'value':'14-12-1980']) }} + * </code> + */ + public function inputDateTime(string! name, array parameters = []) -> string + { + return this->renderInput("datetime", name, parameters); + } + + /** + * Builds a HTML input[type='datetime-local'] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->inputDateTimeLocal( + * [ + * 'name' => 'born', + * 'value' => '14-12-1980', + * ] + * ); + * </code> + * + * Volt syntax: + * <code> + * {{ input_date_time_local(['name':'born', 'value':'14-12-1980']) }} + * </code> + */ + public function inputDateTimeLocal(string! name, array parameters = []) -> string + { + return this->renderInput("datetime-local", name, parameters); + } + + /** + * Builds a HTML input[type='email'] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->inputEmail( + * [ + * 'name' => 'email', + * ] + * ); + * </code> + * + * Volt syntax: + * <code> + * {{ input_email(['name': 'email']); + * </code> + */ + public function inputEmail(string! name, array parameters = []) -> string + { + return this->renderInput("email", name, parameters); + } + + /** + * Builds a HTML input[type='file'] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->inputFile( + * [ + * 'name' => 'file', + * ] + * ); + * </code> + * + * Volt syntax: + * <code> + * {{ input_file(['name': 'file']); + * </code> + */ + public function inputFile(string! name, array parameters = []) -> string + { + return this->renderInput("file", name, parameters); + } + + /** + * Builds a HTML input[type='hidden'] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->inputHidden( + * [ + * 'name' => 'my-field', + * 'value' => 'mike', + * ] + * ); + * </code> + */ + public function inputHidden(string! name, array parameters = []) -> string + { + return this->renderInput("hidden", name, parameters); + } + + /** + * Builds a HTML input[type="image"] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * echo $tag->inputImage( + * [ + * 'src' => '/img/button.png', + * ] + * ); + * </code> + * + * Volt syntax: + * <code> + * {{ input_image(['src': '/img/button.png']) }} + * </code> + */ + public function inputImage(string! name, array parameters = []) -> string + { + return this->renderInput("image", name, parameters); + } + + /** + * Builds a HTML input[type='month'] tag + */ + public function inputMonth(string! name, array parameters = []) -> string + { + return this->renderInput("month", name, parameters); + } + + /** + * Builds a HTML input[type='number'] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->numericField( + * [ + * 'name' => 'price', + * 'min' => '1', + * 'max' => '5', + * ] + * ); + * </code> + */ + public function inputNumeric(string! name, array parameters = []) -> string + { + return this->renderInput("numeric", name, parameters); + } + + /** + * Builds a HTML input[type='password'] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->passwordField( + * [ + * 'name' => 'my-field', + * 'size' => 30, + * ] + * ); + * </code> + */ + public function inputPassword(string! name, array parameters = []) -> string + { + return this->renderInput("password", name, parameters); + } + + /** + * Builds a HTML input[type="radio"] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->inputRadio( + * [ + * 'name' => 'weather', + * 'value" => 'hot', + * ] + * ); + * </code> + * + * Volt syntax: + * <code> + * {{ input_radio(['name': 'weather', 'value": 'hot']) }} + * </code> + */ + public function inputRadio(string! name, array parameters = []) -> string + { + return this->renderInputChecked("radio", name, parameters); + } + + /** + * Builds a HTML input[type='range'] tag + */ + public function inputRange(string! name, array parameters = []) -> string + { + return this->renderInput("range", name, parameters); + } + + /** + * Builds a HTML input[type='search'] tag + */ + public function inputSearch(string! name, array parameters = []) -> string + { + return this->renderInput("search", name, parameters); + } + + /** + * Builds a HTML input[type='tel'] tag + */ + public function inputTel(string! name, array parameters = []) -> string + { + return this->renderInput("tel", name, parameters); + } + + /** + * Builds a HTML input[type='text'] tag + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->inputText( + * [ + * 'name' => 'my-field', + * 'size' => 30, + * ] + * ); + * </code> + */ + public function inputText(string! name, array parameters = []) -> string + { + return this->renderInput("text", name, parameters); + } + + /** + * Builds a HTML input[type='time'] tag + */ + public function inputTime(string! name, array parameters = []) -> string + { + return this->renderInput("time", name, parameters); + } + + /** + * Builds a HTML input[type='url'] tag + */ + public function inputUrl(string! name, array parameters = []) -> string + { + return this->renderInput("url", name, parameters); + } + + /** + * Builds a HTML input[type='week'] tag + */ + public function inputWeek(string! name, array parameters = []) -> string + { + return this->renderInput("week", name, parameters); + } + + /** + * Builds a script[type="javascript"] tag + * + * Parameters + * `local` Local resource or not (default `true`) + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * echo $tag->javascript( + * 'http://ajax.googleapis.com/ajax/libs/jquery/2.2.3/jquery.min.js', + * ['local' => false] + * ); + * echo $tag->javascript('javascript/jquery.js'); + * </code> + * + * Volt syntax: + * <code> + * {{ javascript('http://ajax.googleapis.com/ajax/libs/jquery/2.2.3/jquery.min.js', ['local': false]) }} + * {{ javascript('javascript/jquery.js') }} + * </code> + */ + public function javascript(string url, array parameters = []) -> string + { + var local, service, output; + + let local = (bool) this->arrayGetDefault("local", parameters, true); + + /** + * URLs are generated through the "url" service + */ + if (local === true) { + let service = this->getService("url"), + parameters["src"] = service->getStatic(url); + } else { + let parameters["src"] = url; + } + + unset parameters["local"]; + + let parameters["type"] = this->arrayGetDefault("type", parameters, "text/javascript"), + output = this->renderAttributes("<script", parameters) ."></script>" . PHP_EOL; + + return output; + + } + + /** + * Builds a HTML A tag using framework conventions + * + * Parameters + * `local` Local resource or not (default `true`) + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->link('signup/register', 'Register Here!'); + * + * echo $tag->link( + * 'signup/register', + * 'Register Here!', + * [ + * 'class' => 'btn-primary', + * ] + * ); + * + * echo $tag->link( + * 'https://phalconphp.com/', + * 'Phalcon!', + * [ + * 'local' => false, + * ] + * ); + * + * echo $tag->linkTo( + * 'https://phalconphp.com/', + * 'Phalcon!', + * [ + * 'local' => false, + * 'target' => '_new' + * ] + * ); + * + *</code> + */ + public function link(string url, string text = "", array parameters = []) -> string + { + var local, query, output, service, text; + + let service = this->getService("url"), + url = this->arrayGetDefault("url", parameters, url), + text = this->arrayGetDefault("text", parameters, text), + local = this->arrayGetDefault("local", parameters, true), + query = this->arrayGetDefault("query", parameters, null); + + unset parameters["url"]; + unset parameters["local"]; + unset parameters["text"]; + unset parameters["query"]; + + let parameters["href"] = service->get(url, query, local); + + let output = this->renderAttributes("<a", parameters) . ">" . text . "</a>"; + + return output; + } + + /** + * Prepends a text to current document title + */ + public function prependTitle(array title) -> <Tag> + { + let this->prepend = title; + + return this; + } + + /** + * Renders the title with title tags. The title is automaticall escaped + * + * <code> + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * $tag + * ->prependTitle('Hello') + * ->setTitle('World') + * ->appendTitle('from Phalcon'); + * + * echo $tag->renderTitle(); // <title>Hello World From Phalcon + * + * + * + * {{ render_title() }} + * + */ + public function renderTitle() -> string + { + return "" . this->getTitle() . "" . PHP_EOL; + } + + /** + * Builds a HTML input[type="reset"] tag + * + * + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->reset('Reset') + * + * + * Volt syntax: + * + * {{ reset('Save') }} + * + */ + public function reset(string! name, array parameters = []) -> string + { + return this->renderInput("reset", name, parameters); + } + + /** + * Builds a select element. It accepts an array or a resultset from + * a Phalcon\Mvc\Model + * + * + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->select( + * 'status', + * [ + * 'id' => 'status-id', + * 'useEmpty' => true, + * 'emptyValue => '', + * 'emptyText' => 'Choose Status...', + * ], + * [ + * 'A' => 'Active', + * 'I' => 'Inactive', + * ] + * ); + * + * echo $tag->select( + * 'status', + * [ + * 'id' => 'status-id', + * 'useEmpty' => true, + * 'emptyValue => '', + * 'emptyText' => 'Choose Type...', + * 'using' => [ + * 'id, + * 'name', + * ], + * ], + * Robots::find( + * [ + * 'conditions' => 'type = :type:', + * 'bind' => [ + * 'type' => 'mechanical', + * ] + * ] + * ) + * ); + * + * + * + * @param array parameters + * @param array data + */ + public function select(string! name, array parameters = [], data = null) -> string + { + var emptyText, emptyValue, id, output, outputEmpty, useEmpty, using, value; + + let id = this->arrayGetDefault("id", parameters, name), + name = this->arrayGetDefault("name", parameters, name), + useEmpty = this->arrayGetDefault("useEmpty", parameters, false), + using = [], + parameters["name"] = name, + parameters["id"] = id, + outputEmpty = ""; + + /** + * First check the data passed. We only accept datasets or arrays + */ + if typeof data !== "array" && data !== "object" { + throw new Exception("The dataset must be either an array or a ResultsetInterface"); + } + + /** + * For the ResultsetInterface we need the 'using' parameter + */ + if typeof data === "object" { + let using = this->arrayGetDefault("using", parameters, []); + if typeof using === "array" && count(using) === 2 { + unset parameters["using"]; + } else { + throw new Exception("The 'using' parameter is not a valid array"); + } + } + + /** + * Check if `useEmpty` has been passed + */ + if useEmpty { + let emptyText = this->arrayGetDefault("emptyText", parameters, "Choose..."), + emptyValue = this->arrayGetDefault("emptyValue", parameters, ""), + outputEmpty = sprintf( + "\t" . PHP_EOL, + emptyValue, + emptyText + ); + + unset parameters["useEmpty"]; + unset parameters["emptyText"]; + unset parameters["emptyValue"]; + } + + if !fetch value, parameters["value"] { + let value = this->getValue(id, parameters); + } else { + unset parameters["value"]; + } + + let output = this->renderAttributes("" . PHP_EOL + . outputEmpty; + + if typeof data == "object" { + /** + * Create the SELECT's option from a resultset + */ + let output .= this->renderSelectResultset(data, using, value, "" . PHP_EOL); + } elseif typeof data == "array" { + /** + * Create the SELECT's option from an array + */ + let output .= this->renderSelectArray(data, value, "" . PHP_EOL); + } else { + throw new Exception("Invalid data provided to SELECT helper"); + } + + let output .= ""; + + return output; + } + + /** + * Assigns default values to generated tags by helpers + * + * + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * // Assigning 'peter' to 'name' component + * $tag->setAttribute('name', 'peter'); + * + * // Later in the view + * echo $tag->inputText('name'); // Will have the value 'peter' by default + * + */ + public function setAttribute(string! name, value) -> + { + if value !== null { + if typeof value == "array" || typeof value == "object" { + throw new Exception("Only scalar values can be assigned to UI components"); + } + } + + let this->values[name] = value; + + return this; + } + + /** + * Assigns default values to generated tags by helpers + * + * + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * // Assigning 'peter' to 'name' component + * $tag->setAttribute( + * [ + * 'name' => 'peter', + * ] + * ); + * + * // Later in the view + * echo $tag->inputText('name'); // Will have the value 'peter' by default + * + */ + public function setAttributes(array! values, bool merge = false) -> + { + if merge { + let this->values = array_merge(this->values, values); + } else { + let this->values = values; + } + + return this; + } + + /** + * Sets the dependency injector + */ + public function setDI( container) -> void + { + let this->container = container; + } + + /** + * Set the document type of content + * + * @param int doctype A valid doctype for the content + * + * @return + */ + public function setDocType(int doctype) -> + { + if (doctype < self::HTML32 || doctype > self::XHTML5) { + let this->docType = self::HTML5; + } else { + let this->docType = doctype; + } + + return this; + } + + /** + * Set the title separator of view content + * + * + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * $tag->setTitle('Phalcon Framework'); + * + */ + public function setTitle(string title) -> + { + let this->title = title; + + return this; + } + + /** + * Set the title separator of view content + * + * + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->setTitleSeparator('-'); + * + */ + public function setTitleSeparator(string separator) -> + { + let this->separator = separator; + + return this; + } + + /** + * Builds a LINK[rel="stylesheet"] tag + * + * Parameters + * `local` Local resource or not (default `true`) + * + * + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * echo $tag->stylesheet( + * 'http://fonts.googleapis.com/css?family=Rosario', + * ['local' => false] + * ); + * echo $tag->stylesheet('css/style.css'); + * + * + * Volt syntax: + * + * {{ stylesheet('http://fonts.googleapis.com/css?family=Rosario', ['local': false]) }} + * {{ stylesheet('css/style.css') }} + * + */ + public function stylesheet(string url, array parameters = []) -> string + { + var local, service, output; + + let local = (bool) this->arrayGetDefault("local", parameters, true); + + unset parameters["local"]; + + /** + * URLs are generated through the "url" service + */ + if (local === true) { + let service = this->getService("url"), + parameters["href"] = service->getStatic(url); + } else { + let parameters["href"] = url; + } + + if !isset parameters["rel"] { + let parameters["rel"] = "stylesheet"; + } + + let parameters["type"] = this->arrayGetDefault("type", parameters, "text/css"), + output = this->renderAttributes("renderCloseTag(true); + + return output; + } + + /** + * Builds a HTML input[type="submit"] tag + * + * + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->submit('Save') + * + * + * Volt syntax: + * + * {{ submit('Save') }} + * + */ + public function submit(string! name, array parameters = []) -> string + { + return this->renderInput("submit", name, parameters); + } + + /** + * Builds a HTML TEXTAREA tag + * + * + * use Phalcon\Html\Tag; + * + * $tag = new Tag(); + * + * echo $tag->textArea( + * 'comments', + * [ + * 'cols' => 10, + * 'rows' => 4, + * ] + * ); + * + * + * Volt syntax: + * + * {{ text_area('comments', ['cols': 10, 'rows': 4]) }} + * + */ + public function textArea(string! name, array parameters = []) -> string + { + var content, output; + + let parameters["id"] = this->arrayGetDefault("id", parameters, name), + parameters["name"] = this->arrayGetDefault("name", parameters, name); + + if isset parameters["value"] { + let content = parameters["value"]; + unset parameters["value"]; + } else { + let content = this->getValue(parameters["id"], parameters); + } + + + let output = this->renderAttributes("" . + htmlspecialchars(content) . ""; + + return output; + } + + /** + * Helper method to check an array for an element. If it exists it returns it, + * if not, it returns the supplied default value + */ + private function arrayGetDefault(string name, array parameters, var defaultValue = null) -> var + { + var value; + + if likely fetch value, parameters[name] { + return value; + } + + return defaultValue; + } + + /** + * Returns the escaper service from the DI container + */ + private function getService(string name) + { + var service, container; + + if ("escaper" === name) { + let service = this->escaper; + } else { + let service = this->url; + } + + if typeof service !== "object" { + let container = this->getDI(); + + if typeof container != "object" { + throw new Exception("A dependency injector container is required to obtain the '" . name . "' service"); + } + + if ("escaper" === name) { + let service = container->getShared(name), + this->escaper = service; + } else { + let service = container->getShared(name), + this->url = service; + } + } + + return service; + } + + /** + * Renders the attributes of an HTML element + */ + private function renderAttributes(string! code, array! attributes) -> string + { + var attrs, escaper, escaped, key, newCode, intersect, order, value; + + let order = [ + "rel" : null, + "type" : null, + "for" : null, + "src" : null, + "href" : null, + "action" : null, + "id" : null, + "name" : null, + "value" : null, + "class" : null + ]; + + let intersect = array_intersect_key(order, attributes), + attrs = array_merge(intersect, attributes), + escaper = this->getService("escaper"); + + unset attrs["escape"]; + + let newCode = code; + for key, value in attrs { + if typeof key == "string" && value !== null { + if typeof value == "array" || typeof value == "resource" { + throw new Exception( + "Value at index: '" . key . "' type: '" . gettype(value) . "' cannot be rendered" + ); + } + if escaper { + let escaped = escaper->escapeHtmlAttr(value); + } else { + let escaped = value; + } + let newCode .= " " . key . "=\"" . escaped . "\""; + } + } + + return newCode; + } + + /** + * Returns the closing tag depending on the doctype + */ + private function renderCloseTag(bool addEol = false) -> string + { + var eol = ""; + + if addEol { + let eol = PHP_EOL; + } + + /** + * Check if Doctype is XHTML + */ + if this->docType > self::HTML5 { + return " />" . eol; + } else { + return ">" . eol; + } + } + + /** + * Builds `input` elements + */ + private function renderInput(string type, string name, array parameters = []) -> string + { + var name, id, output; + + let id = this->arrayGetDefault("id", parameters, name); + + let parameters["id"] = id, + parameters["name"] = name, + parameters["type"] = type, + parameters["value"] = this->getValue(id, parameters); + + let output = this->renderAttributes("renderCloseTag(); + + return output; + } + /** + * Builds INPUT tags that implements the checked attribute + */ + private function renderInputChecked(string type, string name, array parameters = []) -> string + { + var currentValue, id, name, output, value; + + let id = this->arrayGetDefault("id", parameters, name); + + /** + * Automatically check inputs + */ + if fetch currentValue, parameters["value"] { + unset parameters["value"]; + + let value = this->getValue(id, parameters); + + if value !== null && currentValue === value { + let parameters["checked"] = "checked"; + } + let parameters["value"] = currentValue; + } else { + let value = this->getValue(id, parameters); + + /** + * Evaluate the value in POST + */ + if value !== null { + let parameters["checked"] = "checked"; + } + + /** + * Update the value anyways + */ + let parameters["value"] = value; + } + + let parameters["id"] = id, + parameters["name"] = name, + parameters["type"] = type; + + let output = this->renderAttributes("renderCloseTag(); + + return output; + } + + /** + * Generates the option values or optgroup from an array + */ + private function renderSelectArray(array options, var value, string closeOption) -> string + { + var label, strOptionValue, strValue, optionText, optionValue, output; + + let output = ""; + + for optionValue, optionText in options { + let label = htmlspecialchars(optionValue); + + /** + * Check if this is an option group + */ + if typeof optionText === "array" { + let output .= "\t" . PHP_EOL + . this->renderSelectArray(optionText, value, closeOption) . "\t" . PHP_EOL; + continue; + } + + if typeof value === "array" { + if in_array(optionValue, value) { + let output .= "\t