Autodoc
  • Namespace
  • Class
  • Tree

Namespaces

  • BlueTihi
    • Context
  • Brickrouge
    • Element
      • Nodes
    • Renderer
    • Widget
  • ICanBoogie
    • ActiveRecord
    • AutoConfig
    • CLDR
    • Composer
    • Core
    • Event
    • Exception
    • HTTP
      • Dispatcher
      • Request
    • I18n
      • Translator
    • Mailer
    • Modules
      • Taxonomy
        • Support
      • Thumbnailer
        • Versions
    • Object
    • Operation
      • Dispatcher
    • Prototype
    • Routes
    • Routing
      • Dispatcher
    • Session
  • Icybee
    • ActiveRecord
      • Model
    • ConfigOperation
    • Document
    • EditBlock
    • Element
      • ActionbarContextual
      • ActionbarSearch
      • ActionbarToolbar
    • FormBlock
    • Installer
    • ManageBlock
    • Modules
      • Articles
      • Cache
        • Collection
        • ManageBlock
      • Comments
        • ManageBlock
      • Contents
        • ManageBlock
      • Dashboard
      • Editor
        • Collection
      • Files
        • File
        • ManageBlock
      • Forms
        • Form
        • ManageBlock
      • I18n
      • Images
        • ManageBlock
      • Members
      • Modules
        • ManageBlock
      • Nodes
        • ManageBlock
        • Module
      • Pages
        • BreadcrumbElement
        • LanguagesElement
        • ManageBlock
        • NavigationBranchElement
        • NavigationElement
        • Page
        • PageController
      • Registry
      • Search
      • Seo
      • Sites
        • ManageBlock
      • Taxonomy
        • Terms
          • ManageBlock
        • Vocabulary
          • ManageBlock
      • Users
        • ManageBlock
        • NonceLogin
        • Roles
      • Views
        • ActiveRecordProvider
        • Collection
        • View
    • Operation
      • ActiveRecord
      • Constructor
      • Module
      • Widget
    • Rendering
  • None
  • Patron
  • PHP

Classes

  • AlterColumnsEvent
  • AlterQueryEvent
  • AlterRenderedCellsEvent
  • BooleanCellRenderer
  • BooleanColumn
  • CellRenderer
  • Column
  • DateColumn
  • DateTimeColumn
  • EditColumn
  • EditDecorator
  • FilterDecorator
  • HeaderRenderer
  • KeyColumn
  • Options
  • RegisterColumnsEvent
  • SizeColumn
  • Translator
   1 <?php
   2 
   3 /*
   4  * This file is part of the Icybee package.
   5  *
   6  * (c) Olivier Laviale <olivier.laviale@gmail.com>
   7  *
   8  * For the full copyright and license information, please view the LICENSE
   9  * file that was distributed with this source code.
  10  */
  11 
  12 namespace Icybee;
  13 
  14 use ICanBoogie\ActiveRecord\Model;
  15 use ICanBoogie\ActiveRecord\Query;
  16 use ICanBoogie\I18n;
  17 use ICanBoogie\Operation;
  18 
  19 use Brickrouge\Alert;
  20 use Brickrouge\Button;
  21 use Brickrouge\Element;
  22 use Brickrouge\Form;
  23 use Brickrouge\Ranger;
  24 use Brickrouge\Text;
  25 
  26 use Icybee\Element\ActionbarSearch;
  27 use Icybee\ManageBlock\Column;
  28 use Icybee\ManageBlock\Options;
  29 use Icybee\ManageBlock\Translator;
  30 use Icybee\Element\ActionbarContextual;
  31 
  32 /* @var $column \Icybee\ManageBlock\Column */
  33 
  34 /**
  35  * An element to manage the records of a module.
  36  *
  37  * @property-read \ICanBoogie\ActiveRecord\Model $model
  38  * @property-read string $primary_key The primary key of the records.
  39  * @property-read Options $options The display options.
  40  * @property-read bool $is_filtering `true` if records are filtered.
  41  * @property-read Translator $t The translator used by the element.
  42  *
  43  * @changes-20130622
  44  *
  45  * - All extend_column* methods are removed.
  46  * - alter_range_query() signature changed, $options is now an instance of Options an not an array.
  47  * - AlterColumnsEvent has been redesigned, `records` is removed.
  48  *
  49  * @TODO-20130626:
  50  *
  51  * - [filters][options] -> [filter_options]
  52  * - throw error when COLUMNS_ORDER use an undefined column.
  53  */
  54 class ManageBlock extends Element
  55 {
  56     const DISCREET_PLACEHOLDER = '<span class="lighter">―</span>';
  57 
  58     const T_BLOCK = '#manager-block';
  59     const T_COLUMNS_ORDER = '#manager-columns-order';
  60     const T_ORDER_BY = '#manager-order-by';
  61 
  62     #
  63     # sort constants
  64     #
  65 
  66     const ORDER_ASC = 'asc';
  67     const ORDER_DESC = 'desc';
  68 
  69     static protected function add_assets(\Brickrouge\Document $document)
  70     {
  71         parent::add_assets($document);
  72 
  73         $document->js->add('manage.js', -170);
  74         $document->css->add(\Icybee\ASSETS . 'css/manage.css', -170);
  75 
  76         $document->js->add('manage/operations.js', -170);
  77     }
  78 
  79     /**
  80      * Currently used module.
  81      *
  82      * @var \ICanBoogie\Module
  83      */
  84     public $module;
  85 
  86     /**
  87      * Currently used model.
  88      *
  89      * @var \ICanBoogie\ActiveRecord\Model
  90      */
  91     protected $model;
  92 
  93     /**
  94      * Returns the {@link $model} property.
  95      *
  96      * @return \ICanBoogie\ActiveRecord\Model
  97      */
  98     protected function get_model()
  99     {
 100         return $this->model;
 101     }
 102 
 103     /**
 104      * The columns of the element.
 105      *
 106      * @var array[string]Column
 107      */
 108     protected $columns;
 109 
 110     /**
 111      * The records to display.
 112      *
 113      * @var array[]ActiveRecord
 114      */
 115     protected $records;
 116 
 117     /**
 118      * The total number of records matching the filters.
 119      *
 120      * @var int
 121      */
 122     protected $count;
 123 
 124     /**
 125      * Returns the primary key of the records.
 126      *
 127      * @var string
 128      */
 129     protected function get_primary_key()
 130     {
 131         return $this->model->primary;
 132     }
 133 
 134     /**
 135      * Jobs that can be applied to the records.
 136      *
 137      * @var array[string]mixed
 138      */
 139     protected $jobs = array();
 140 
 141     protected $browse;
 142 
 143     /**
 144      * Proxis translator with the following scope: "manager.<module_flat_id>"
 145      *
 146      * @var \ICanBoogie\I18n\Translator\Proxi
 147      */
 148     protected $t;
 149 
 150     /**
 151      * Returns the {@link $t} property.
 152      *
 153      * @return \ICanBoogie\I18n\Translator\Proxi
 154      */
 155     protected function get_t()
 156     {
 157         return $this->t;
 158     }
 159 
 160     /**
 161      * Display options.
 162      *
 163      * @var Options
 164      */
 165     protected $options;
 166 
 167     /**
 168      * Returns the {@link $options} property.
 169      *
 170      * @return \Icybee\ManageBlock\Options
 171      */
 172     protected function get_options()
 173     {
 174         return $this->options;
 175     }
 176 
 177     public function __construct(Module $module, array $attributes)
 178     {
 179         ## 20130625: checking deprecated methods
 180 
 181         if (method_exists($this, 'get_query_conditions'))
 182         {
 183             throw new \Exception("The <q>get_query_conditions()</q> method is deprecated. Use <q>alter_query()</q> instead.");
 184         }
 185 
 186         if (method_exists($this, 'extend_column'))
 187         {
 188             throw new \Exception("The <q>extend_column()</q> method is deprecated. Define columns with classes.");
 189         }
 190 
 191         if (method_exists($this, 'extend_columns'))
 192         {
 193             throw new \Exception("The <q>extend_columns()</q> method is deprecated. Define columns with classes.");
 194         }
 195 
 196         if (method_exists($this, 'retrieve_options'))
 197         {
 198             throw new \Exception("The <q>retrieve_options()</q> method is deprecated. Use <q>resolve_options()</q>.");
 199         }
 200 
 201         if (method_exists($this, 'store_options'))
 202         {
 203             throw new \Exception("The <q>store_options()</q> method is deprecated. Use the Options instance.");
 204         }
 205 
 206         if (method_exists($this, 'alter_range_query'))
 207         {
 208             throw new \Exception("The <q>alter_range_query()</q> method is deprecated. Use columns and <q>alter_query_with_limit()</q>.");
 209         }
 210 
 211         if (method_exists($this, 'load_range'))
 212         {
 213             throw new \Exception("The <q>load_range()</q> method is deprecated. Use <q>fetch_records()</q>.");
 214         }
 215 
 216         if (method_exists($this, 'parseColumns'))
 217         {
 218             throw new \Exception("The <q>parseColumns()</q> method is deprecated. Use <q>resolve_columns()</q>.");
 219         }
 220 
 221         if (method_exists($this, 'columns'))
 222         {
 223             throw new \Exception("The <q>columns()</q> method is deprecated. Use <q>get_available_columns()</q>.");
 224         }
 225 
 226         if (method_exists($this, 'jobs'))
 227         {
 228             throw new \Exception("The <q>jobs()</q> method is deprecated. Use <q>get_available_jobs()</q>.");
 229         }
 230 
 231         if (method_exists($this, 'addJob'))
 232         {
 233             throw new \Exception("The <q>addJob()</q> method is deprecated. Use <q>resolve_jobs()</q>.");
 234         }
 235 
 236         if (method_exists($this, 'getJobs'))
 237         {
 238             throw new \Exception("The <q>getJobs()</q> method is deprecated. Use <q>render_jobs()</q>.");
 239         }
 240 
 241         if (method_exists($this, 'render_limiter'))
 242         {
 243             throw new \Exception("The <q>render_limiter()</q> method is deprecated. Use <q>render_controls()</q>.");
 244         }
 245 
 246         $class_reflection = new \ReflectionClass($this);
 247 
 248         foreach ($class_reflection->getMethods() as $method_reflection)
 249         {
 250             if (strpos($method_reflection->name, 'extend_column_') === 0)
 251             {
 252                 throw new \Exception("The <q>{$method_reflection->name}</q> method is deprecated. Use a column class.");
 253             }
 254 
 255             if (strpos($method_reflection->name, 'render_column_') === 0)
 256             {
 257                 throw new \Exception("The <q>{$method_reflection->name}</q> method is deprecated. Use a column class.");
 258             }
 259 
 260             if (strpos($method_reflection->name, 'render_cell_') === 0)
 261             {
 262                 throw new \Exception("The <q>{$method_reflection->name}</q> method is deprecated. Use a column class.");
 263             }
 264         }
 265 
 266         ## /20130625
 267 
 268         parent::__construct('div', $attributes + array('class' => 'listview listview-interactive'));
 269 
 270         $this->module = $module;
 271         $this->model = $module->model;
 272         $this->t = new Translator($module);
 273         $this->columns = $this->get_columns();
 274         $this->jobs = $this->get_jobs();
 275     }
 276 
 277     /**
 278      * Returns the available columns.
 279      *
 280      * @return array[string]mixed
 281      */
 282     protected function get_available_columns()
 283     {
 284         $primary_key = $this->model->primary;
 285 
 286         if ($primary_key)
 287         {
 288             return array($primary_key => 'Icybee\ManageBlock\KeyColumn');
 289         }
 290 
 291         return array();
 292     }
 293 
 294     protected function get_columns()
 295     {
 296         $columns = $this->get_available_columns();
 297 
 298         new \Icybee\ManageBlock\RegisterColumnsEvent($this, $columns);
 299 
 300         $columns = $this->resolve_columns($columns);
 301 
 302         new \Icybee\ManageBlock\AlterColumnsEvent($this, $columns);
 303 
 304         foreach ($columns as $column_id => $column)
 305         {
 306             if ($column instanceof Column)
 307             {
 308                 continue;
 309             }
 310 
 311             throw new \UnexpectedValueException(\ICanBoogie\format
 312             (
 313                 'Column %id must be an instance of Column. Given: %type. :data', array
 314                 (
 315                     '%id' => $column_id,
 316                     '%type' => gettype($column),
 317                     ':data' => $column
 318                 )
 319             ));
 320         }
 321 
 322         return $columns;
 323     }
 324 
 325     protected function resolve_columns(array $columns)
 326     {
 327         $columns_order = $this[self::T_COLUMNS_ORDER];
 328 
 329         if ($columns_order)
 330         {
 331             $primary = $this->model->primary;
 332 
 333             if ($primary)
 334             {
 335                 array_unshift($columns_order, $primary);
 336             }
 337 
 338             $columns_order = array_combine($columns_order, array_fill(0, count($columns_order), null));
 339             $columns = array_intersect_key($columns, $columns_order);
 340             $columns = array_merge($columns_order, $columns);
 341         }
 342 
 343         $resolved_columns = array();
 344 
 345         foreach ($columns as $id => $options)
 346         {
 347             if ($options === null)
 348             {
 349                 throw new \Exception(\ICanBoogie\format("Column %id is not defined.", array('id' => $id)));
 350             }
 351 
 352             $construct = __CLASS__ . '\Column';
 353 
 354             if (is_string($options))
 355             {
 356                 $construct = $options;
 357                 $options = array();
 358             }
 359 
 360             $resolved_columns[$id] = new $construct($this, $id, $options);
 361         }
 362 
 363         return $resolved_columns;
 364     }
 365 
 366     /**
 367      * Returns the available jobs.
 368      *
 369      * @return array[string]mixed
 370      */
 371     protected function get_available_jobs()
 372     {
 373         return array();
 374     }
 375 
 376     /**
 377      * Returns the jobs.
 378      *
 379      * @return array[string]mixed
 380      */
 381     protected function get_jobs()
 382     {
 383         $jobs = $this->get_available_jobs();
 384         $jobs = $this->resolve_jobs($jobs);
 385 
 386         return $jobs;
 387     }
 388 
 389     /**
 390      * Resolves the available jobs.
 391      *
 392      * @param array $jobs
 393      *
 394      * @return array
 395      */
 396     protected function resolve_jobs(array $jobs)
 397     {
 398         if ($this->primary_key)
 399         {
 400             $jobs = array_merge(array(Module::OPERATION_DELETE => $this->t('delete.operation.short_title')), $jobs);
 401         }
 402 
 403         return $jobs;
 404     }
 405 
 406     /**
 407      * Update filters with the specified modifiers.
 408      *
 409      * The extended schema of the model is used to automatically handle booleans, integers,
 410      * dates (date, datetime and timestamp) and strings (char, varchar).
 411      *
 412      * @param array $filters
 413      * @param array $modifiers
 414      *
 415      * @return array Updated filters.
 416      */
 417     protected function update_filters(array $filters, array $modifiers)
 418     {
 419         static $as_strings = array('char', 'varchar', 'date', 'datetime', 'timestamp');
 420 
 421         $fields = $this->model->extended_schema['fields'];
 422 
 423         foreach ($modifiers as $identifier => $value)
 424         {
 425             if (empty($fields[$identifier]))
 426             {
 427                 continue;
 428             }
 429 
 430             $type = $fields[$identifier]['type'];
 431 
 432             if ($type == 'boolean')
 433             {
 434                 $value = $value === '' ? null : filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
 435             }
 436             else if ($type == 'integer')
 437             {
 438                 $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
 439             }
 440             else if (in_array($type, $as_strings))
 441             {
 442                 if ($value === '')
 443                 {
 444                     $value = null;
 445                 }
 446             }
 447             else continue;
 448 
 449             if ($value === null)
 450             {
 451                 unset($filters[$identifier]);
 452 
 453                 continue;
 454             }
 455 
 456             $filters[$identifier] = $value;
 457         }
 458 
 459         foreach ($this->columns as $id => $column)
 460         {
 461             $filters = $column->alter_filters($filters, $modifiers);
 462         }
 463 
 464         return $filters;
 465     }
 466 
 467     /**
 468      * Updates options with the provided modifiers.
 469      *
 470      * The method updates the `order`, `start`, `limit`, `search` and `filters` options.
 471      *
 472      * The `start` options is reset to 1 when the `order`, `search` or `filters` options change.
 473      *
 474      * @param array $options Previous options.
 475      * @param array $modifiers Options modifiers.
 476      *
 477      * @return array Updated options.
 478      */
 479     protected function update_options(Options $options, array $modifiers)
 480     {
 481         $modifiers['filters'] = $this->update_filters($options->filters, $modifiers);
 482 
 483         return $options->update($modifiers);
 484     }
 485 
 486     /**
 487      * Resolves the display order of the records according to the default options and the
 488      * available columns.
 489      *
 490      * If the column that should be used to order the records does not exists, the order is
 491      * reseted.
 492      *
 493      * If the order direction if not defined, the default direction of the column if used
 494      * instead.
 495      *
 496      * @param string|null $order_by The identifier of the column used to order the records.
 497      * @param string|int|null $order_direction The direction of the ordering. One of: "asc",
 498      * "desc", 1, -1 or `null`.
 499      *
 500      * @return array Returns an array made of the column identifier and the order direction.
 501      */
 502     protected function resolve_order($order_by, $order_direction)
 503     {
 504         $columns = $this->columns;
 505         $default_order = $this[self::T_ORDER_BY];
 506 
 507         if (!$order_by && $default_order)
 508         {
 509             list($order_by, $order_direction) = (array) $default_order + array(1 => 'desc');
 510 
 511             $order_direction = ($order_direction == 'desc') ? -1 : 1;
 512         }
 513 
 514         if ($order_by && empty($columns[$order_by]))
 515         {
 516             \ICanBoogie\log_error("Undefined column for order: !order.", array('order' => $order_by));
 517 
 518             $order_by = null;
 519             $order_direction = null;
 520         }
 521 
 522         if (!$order_direction && isset($columns[$order_by]))
 523         {
 524             $order_direction = $columns[$order_by]->default_order;
 525         }
 526 
 527         return array($order_by, $order_direction);
 528     }
 529 
 530     /**
 531      * Returns the options for the element.
 532      *
 533      * Options are restored from the storing backend and updated according to the supplied
 534      * modifiers.
 535      *
 536      * @param array $modifiers
 537      *
 538      * @return Options
 539      */
 540     protected function resolve_options($name, array $modifiers)
 541     {
 542         $options = new Options($name);
 543         $options->retrieve();
 544         $options = $this->update_options($options, $modifiers);
 545 
 546         list($order_by, $order_direction) = $this->resolve_order($options->order_by, $options->order_direction);
 547 
 548         $options->order_by = $order_by;
 549         $options->order_direction = $order_direction;
 550         $options->store();
 551 
 552         return $options;
 553     }
 554 
 555     /**
 556      * Renders the element.
 557      *
 558      * If an error occurd while creating the query or fecthing the records, the filters and the
 559      * order are reseted.
 560      */
 561     public function render()
 562     {
 563         global $core;
 564 
 565         $options = $this->options = $this->resolve_options($this->module->flat_id, $core->request->params);
 566         $order_by = $options->order_by;
 567 
 568         if ($order_by)
 569         {
 570             $order_column = $this->columns[$order_by];
 571             $order_column->order = $options->order_direction;
 572         }
 573 
 574         try
 575         {
 576             $query = $this->resolve_query($options);
 577             $records = $this->fetch_records($query);
 578 
 579             if ($records)
 580             {
 581                 $records = $this->alter_records($records);
 582                 $this->records = array_values($records);
 583             }
 584         }
 585         catch (\Exception $e)
 586         {
 587             $options->order_by = null;
 588             $options->order_direction = null;
 589             $options->filters = array();
 590             $options->store();
 591 
 592             $rendered_exception = \Brickrouge\render_exception($e);
 593 
 594             return <<<EOT
 595 <div class="alert alert-error alert-block undissmisable">
 596     <p>There was an error in the SQL statement, orders and filters have been reseted,
 597     please reload the page.</p>
 598 
 599     $rendered_exception
 600 </div>
 601 EOT;
 602         }
 603 
 604         $html = parent::render();
 605         $document = \Brickrouge\get_document();
 606 
 607         foreach ($this->columns as $column)
 608         {
 609             $column->add_assets($document);
 610         }
 611 
 612         return $html;
 613     }
 614 
 615     /**
 616      * Renders the object into a HTML string.
 617      */
 618     protected function render_inner_html()
 619     {
 620         global $core;
 621 
 622         $records = $this->records;
 623         $options = $this->options;
 624 
 625         if ($records || $options->filters)
 626         {
 627             if ($records)
 628             {
 629                 $body = '<tbody>' . $this->render_body() . '</tbody>';
 630             }
 631             else
 632             {
 633                 $body = '<tbody class="empty"><tr><td colspan="' . count($this->columns) . '">' . $this->render_empty_body() . '</td></tr></tbody>';
 634             }
 635 
 636             $head = $this->render_head();
 637             $foot = $this->render_foot();
 638 
 639             $content = <<<EOT
 640 <table>
 641     $head
 642     $foot
 643     $body
 644 </table>
 645 EOT;
 646         }
 647         else
 648         {
 649             $body = $this->render_empty_body();
 650             $foot = $this->render_foot();
 651             $columns_n = count($this->columns);
 652 
 653             $content = <<<EOT
 654 <table>
 655     <tbody class="empty" td colspan="$columns_n">$body</tbody>
 656     $foot
 657 </table>
 658 EOT;
 659         }
 660 
 661         #
 662 
 663         $search = $this->render_search();
 664 
 665         $core->events->attach(function(ActionbarSearch\AlterInnerHTMLEvent $event, ActionbarSearch $sender) use($search)
 666         {
 667             $event->html .= $search;
 668         });
 669 
 670         #
 671 
 672         $rendered_jobs = $this->render_jobs($this->jobs);
 673 
 674         $core->events->attach(function(ActionbarContextual\CollectItemsEvent $event, ActionbarContextual $target) use($rendered_jobs) {
 675 
 676             $event->items[] = $rendered_jobs;
 677 
 678         });
 679 
 680         #
 681 
 682         return $content;
 683     }
 684 
 685     /**
 686      * Wraps the listview in a `form` element.
 687      */
 688     protected function render_outer_html()
 689     {
 690         $html = parent::render_outer_html();
 691 
 692         $operation_name = Operation::DESTINATION;
 693         $operation_value = $this->module->id;
 694 
 695         $block_name = self::T_BLOCK;
 696         $block_value = $this[self::T_BLOCK] ?: 'manage';
 697 
 698         return <<<EOT
 699 <form id="manager" method="GET" action="">
 700     <input type="hidden" name="{$operation_name}" value="{$operation_value}" />
 701     <input type="hidden" name="{$block_name}" value="{$block_value}" />
 702     $html
 703 </form>
 704 EOT;
 705     }
 706 
 707     /**
 708      * Resolve ActiveRecord query according to the supplied options.
 709      *
 710      * Note: The method updates the {@link $count} property with the number of records matching
 711      * the query, before a range is applied.
 712      *
 713      * @param Options $options
 714      *
 715      * @return \ICanBoogie\ActiveRecord\Query
 716      */
 717     protected function resolve_query(Options $options)
 718     {
 719         $query = new Query($this->model);
 720         $query = $this->alter_query($query, $options->filters);
 721 
 722         #
 723 
 724         new ManageBlock\AlterQueryEvent($this, $query, $options);
 725 
 726         #
 727 
 728         $search = $options->search;
 729 
 730         if ($search)
 731         {
 732             $query = $this->alter_query_with_search($query, $search);
 733         }
 734 
 735         #
 736         # Adjust `start` so that it's never greater than `count`.
 737         #
 738 
 739         $start = $options->start;
 740         $count = $this->count = $query->count;
 741 
 742         if ($start > $count)
 743         {
 744             $options->start = 1;
 745             $options->store();
 746         }
 747         else if ($start < -$count)
 748         {
 749             $options->start = 1;
 750             $options->store();
 751         }
 752         else if ($start < 0)
 753         {
 754             $start = -(-($start - 1) % $count) + $count;
 755             $start = ceil($start / $options->limit) * $options->limit + 1;
 756 
 757             $options->start = $start;
 758             $options->store();
 759         }
 760 
 761         $order_by = $options->order_by;
 762 
 763         if ($order_by)
 764         {
 765             $query = $this->columns[$order_by]->alter_query_with_order($query, $options->order_direction);
 766         }
 767 
 768         return $this->alter_query_with_range($query, $options->start - 1, $options->limit);
 769     }
 770 
 771     /**
 772      * Alters the initial query with the specified filters.
 773      *
 774      * The `alter_query` method of each column is invoked in turn to alter the query.
 775      *
 776      * @param Query $query
 777      * @param array $filters
 778      *
 779      * @return Query The altered query.
 780      */
 781     protected function alter_query(Query $query, array $filters)
 782     {
 783         foreach ($this->columns as $id => $column)
 784         {
 785             if (!isset($filters[$id]))
 786             {
 787                 continue;
 788             }
 789 
 790             $query = $column->alter_query_with_filter($query, $filters[$id]);
 791         }
 792 
 793         return $query;
 794     }
 795 
 796     /**
 797      * Alters the query according to a search string.
 798      *
 799      * @param Query $query
 800      * @param string $search
 801      *
 802      * @return Query
 803      */
 804     protected function alter_query_with_search(Query $query, $search)
 805     {
 806         static $supported_types = array('char', 'varchar', 'text');
 807 
 808         $words = explode(' ', $search);
 809         $words = array_map('trim', $words);
 810 
 811         $queries = array();
 812         $fields = $this->model->extended_schema['fields'];
 813 
 814         foreach ($words as $word)
 815         {
 816             $concats = '';
 817 
 818             foreach ($fields as $identifier => $definition)
 819             {
 820                 $type = $definition['type'];
 821 
 822                 if (!in_array($type, $supported_types))
 823                 {
 824                     continue;
 825                 }
 826 
 827                 if (!empty($definition['null']))
 828                 {
 829                     $identifier = "IFNULL(`$identifier`, \"\")";
 830                 }
 831 
 832                 $concats .= ', `' . $identifier . '`';
 833             }
 834 
 835             if (!$concats)
 836             {
 837                 continue;
 838             }
 839 
 840             $query->where('CONCAT_WS(" ", ' . substr($concats, 2) . ') LIKE ?', "%{$word}%");
 841         }
 842 
 843         return $query;
 844     }
 845 
 846     /**
 847      * Alters query with range (offset and limit).
 848      *
 849      * @param Query $query
 850      * @param int $offset The offset of the record to return.
 851      * @param int $limit The maximum number of records to return.
 852      *
 853      * @return Query
 854      */
 855     protected function alter_query_with_range(Query $query, $offset, $limit)
 856     {
 857         return $query->limit($offset, $limit);
 858     }
 859 
 860     /**
 861      * Fetches the records matching the query.
 862      *
 863      * @param Query $query
 864      */
 865     protected function fetch_records(Query $query)
 866     {
 867         return $query->all;
 868     }
 869 
 870     /**
 871      * Alters records.
 872      *
 873      * The function return the records _as is_ but subclasses can implement the method to
 874      * load all the dependencies of the records in a single step.
 875      *
 876      * @param array $records
 877      *
 878      * @return array
 879      */
 880     protected function alter_records(array $records)
 881     {
 882         foreach ($this->columns as $column)
 883         {
 884             $records = $column->alter_records($records);
 885         }
 886 
 887         return $records;
 888     }
 889 
 890     /**
 891      * Renders the THEAD element.
 892      *
 893      * @return string The rendered THEAD element.
 894      */
 895     protected function render_head()
 896     {
 897         $cells = '';
 898 
 899         foreach ($this->columns as $id => $column)
 900         {
 901             $cells .= $this->render_column($column, $id);
 902         }
 903 
 904         return <<<EOT
 905 <thead>
 906     <tr>$cells</tr>
 907 </thead>
 908 EOT;
 909     }
 910 
 911     /**
 912      * Renders a column header.
 913      *
 914      * @param array $cell
 915      * @param string $id
 916      *
 917      * @return string The rendered THEAD cell.
 918      */
 919     protected function render_column(Column $column, $id)
 920     {
 921         $class = 'header--' . \Brickrouge\normalize($id) . ' ' . $column->class;
 922 
 923         if ($this->count > 1 || $this->options->filters || $this->options->search)
 924         {
 925             $orderable = $column->orderable;
 926 
 927             if ($orderable)
 928             {
 929                 $class .= ' orderable';
 930             }
 931 
 932             $filtering = $column->is_filtering;
 933 
 934             if ($filtering)
 935             {
 936                 $class .= ' filtering';
 937             }
 938 
 939             $filters = $column->filters;
 940 
 941             if ($filters)
 942             {
 943                 $class .= ' filters';
 944             }
 945         }
 946         else
 947         {
 948             $orderable = false;
 949             $filtering = false;
 950             $filters = array();
 951         }
 952 
 953         $header_options = $column->render_options();
 954 
 955         if ($header_options)
 956         {
 957             $class .= ' has-options';
 958         }
 959 
 960         $header = $column->render_header();
 961 
 962         if (!$header)
 963         {
 964             $class .= ' has-no-label';
 965         }
 966 
 967         $class = trim($class);
 968 
 969         return <<<EOT
 970 <th class="$class"><div>{$header}{$header_options}</div></th>
 971 EOT;
 972     }
 973 
 974     /**
 975      * Renders the cells of the columns.
 976      *
 977      * The method returns an array with the following layout:
 978      *
 979      *     [<column_id>][] => <cell_content>
 980      *
 981      * @param array $columns The columns to render.
 982      *
 983      * @return array[string]mixed
 984      */
 985     protected function render_columns_cells(array $columns)
 986     {
 987         $rendered_columns_cells = array();
 988 
 989         foreach ($columns as $id => $column)
 990         {
 991             foreach ($this->records as $record)
 992             {
 993                 try
 994                 {
 995                     $content = (string) $column->render_cell($record);
 996                 }
 997                 catch (\Exception $e)
 998                 {
 999                     $content = \Brickrouge\render_exception($e);
1000                 }
1001 
1002                 $rendered_columns_cells[$id][] = $content;
1003             }
1004         }
1005 
1006         return $rendered_columns_cells;
1007     }
1008 
1009     /**
1010      * Replaces repeating values of a column with the discreet placeholder.
1011      *
1012      * @param array $rendered_columns_cells
1013      *
1014      * @return array[string]mixed
1015      */
1016     protected function apply_discreet_filter(array $rendered_columns_cells)
1017     {
1018         $discreet_column_cells = $rendered_columns_cells;
1019         $columns = $this->columns;
1020 
1021         foreach ($discreet_column_cells as $id => &$cells)
1022         {
1023             $column = $columns[$id];
1024 
1025             if (!$column->discreet)
1026             {
1027                 continue;
1028             }
1029 
1030             $last_content = null;
1031 
1032             foreach ($cells as &$content)
1033             {
1034                 if ($last_content !== $content || !$content)
1035                 {
1036                     $last_content = $content;
1037 
1038                     continue;
1039                 }
1040 
1041                 $content = self::DISCREET_PLACEHOLDER;
1042             }
1043         }
1044 
1045         return $discreet_column_cells;
1046     }
1047 
1048     /**
1049      * Convert rendered columns cells to rows.
1050      *
1051      * @param array $rendered_columns_cells
1052      *
1053      * @return array[]array
1054      */
1055     protected function columns_to_rows(array $rendered_columns_cells)
1056     {
1057         $rows = array();
1058 
1059         foreach ($rendered_columns_cells as $column_id => $cells)
1060         {
1061             foreach ($cells as $i => $cell)
1062             {
1063                 $rows[$i][$column_id] = $cell;
1064             }
1065         }
1066 
1067         return $rows;
1068     }
1069 
1070     /**
1071      * Renders the specified rows.
1072      *
1073      * The rows are rendered as an array of {@link Element} instances representing `TR` elements.
1074      *
1075      * @param array $rows
1076      *
1077      * @return array[]Element
1078      */
1079     protected function render_rows(array $rows)
1080     {
1081         global $core;
1082 
1083         $rendered_rows = array();
1084         $columns = $this->columns;
1085         $records = $this->records;
1086         $key = $this->primary_key;
1087         $module = $this->module;
1088         $user = $core->user;
1089 
1090         foreach ($rows as $i => $cells)
1091         {
1092             $html = '';
1093 
1094             foreach ($cells as $column_id => $cell)
1095             {
1096                 $html .= '<td class="'
1097                 . trim('cell--' . \Brickrouge\normalize($column_id) . ' ' . $columns[$column_id]->class)
1098                 . '">' . ($cell ?: '&nbsp;') . '</td>';
1099             }
1100 
1101             $tr = new Element('tr', array(Element::INNER_HTML => $html));
1102 
1103             if ($key && !$user->has_ownership($module, $records[$i]))
1104             {
1105                 $tr->add_class('no-ownership');
1106             }
1107 
1108             $rendered_rows[] = $tr;
1109         }
1110 
1111         return $rendered_rows;
1112     }
1113 
1114     /**
1115      * Renders table body.
1116      *
1117      * @return string
1118      */
1119     protected function render_body()
1120     {
1121         $rendered_cells = $this->render_columns_cells($this->columns);
1122 
1123         new ManageBlock\AlterRenderedCellsEvent($this, $rendered_cells, $this->records);
1124 
1125         $rendered_cells = $this->apply_discreet_filter($rendered_cells);
1126         $rows = $this->columns_to_rows($rendered_cells);
1127         $rendered_rows = $this->render_rows($rows);
1128 
1129         return implode(PHP_EOL, $rendered_rows);
1130     }
1131 
1132     /**
1133      * Renders an alternate body when there is no record to display.
1134      *
1135      * @return \Brickrouge\Alert
1136      */
1137     protected function render_empty_body()
1138     {
1139         $search = $this->options->search;
1140         $filters = $this->options->filters;
1141         $context = null;
1142 
1143         if ($search)
1144         {
1145             $message = $this->t('Your search <q><strong>!search</strong></q> did not match any record.<br /><br /><a href="?q=" rel="manager/search" data-action="reset" class="btn btn-warning">Reset search filter</a>', array('!search' => $search));
1146         }
1147         else if ($filters)
1148         {
1149             // TODO-20130629: column should implement a humanize_filter() method that would return a humanized filter expression.
1150 
1151             $filters = implode(', ', $filters);
1152             $message = $this->t('Your selection <q><strong>!selection</strong></q> dit not match any record.', array('!selection' => $filters));
1153         }
1154         else
1155         {
1156             $message = $this->t('create_first', array('!url' => \ICanBoogie\Routing\contextualize("/admin/{$this->module->id}/new")));
1157             $context = 'info';
1158         }
1159 
1160         return new Alert($message, array(Alert::CONTEXT => $context, 'class' => 'alert listview-alert'));
1161     }
1162 
1163     /**
1164      * Renders the "search" element to be injected in the document.
1165      *
1166      * @return \Brickrouge\Form
1167      */
1168     protected function render_search()
1169     {
1170         $search = $this->options->search;
1171 
1172         return new Element
1173         (
1174             'div', array
1175             (
1176                 Element::CHILDREN => array
1177                 (
1178                     'q' => new Text
1179                     (
1180                         array
1181                         (
1182                             'title' => $this->t('Search in the records'),
1183                             'value' => $search,
1184                             'size' => '16',
1185                             'class' => 'search',
1186                             'tabindex' => 0,
1187 
1188                             'data-placeholder' => $this->t('Search')
1189                         )
1190                     ),
1191 
1192                     new Button
1193                     (
1194                         '', array
1195                         (
1196                             'type' => 'button',
1197                             'class' => 'icon-remove'
1198                         )
1199                     )
1200                 ),
1201 
1202                 'class' => 'listview-search'
1203             )
1204         );
1205     }
1206 
1207     /**
1208      * Renders listview controls.
1209      *
1210      * @return string
1211      */
1212     protected function render_controls()
1213     {
1214         $count = $this->count;
1215         $start = $this->options->start;
1216         $limit = $this->options->limit;
1217 
1218         if ($count <= 10)
1219         {
1220             $content = $this->t($this->is_filtering || $this->options->search ? "records_count_with_filters" : "records_count", array(':count' => $count));
1221 
1222             return <<<EOT
1223 <div class="listview-controls">
1224 $content
1225 </div>
1226 EOT;
1227         }
1228 
1229         $ranger = new Ranger
1230         (
1231             'div', array
1232             (
1233                 Ranger::T_START => $start,
1234                 Ranger::T_LIMIT => $limit,
1235                 Ranger::T_COUNT => $count,
1236                 Ranger::T_EDITABLE => true,
1237                 Ranger::T_NO_ARROWS => true,
1238 
1239                 'class' => 'listview-start'
1240             )
1241         );
1242 
1243         $page_limit_selector = null;
1244 
1245         if ($limit >= 20 || $count >= $limit)
1246         {
1247             $page_limit_selector = new Element
1248             (
1249                 'select', array
1250                 (
1251                     Element::OPTIONS => array(10 => 10, 20 => 20, 50 => 50, 100 => 100),
1252 
1253                     'title' => $this->t('Number of item to display by page'),
1254                     'name' => 'limit',
1255                     'onchange' => 'this.form.submit()',
1256                     'value' => $limit
1257                 )
1258             );
1259 
1260             $page_limit_selector = '<div class="listview-limit">' . $this->t(':page_limit_selector by page', array(':page_limit_selector' => (string) $page_limit_selector)) . '</div>';
1261         }
1262 
1263         $browse = null;
1264 
1265         if ($count > $limit)
1266         {
1267             $browse = <<<EOT
1268 <div class="listview-browse">
1269     <a href="?start=previous" class="browse previous" rel="manager"><i class="icon-arrow-left"></i></a>
1270     <a href="?start=next" class="browse next" rel="manager"><i class="icon-arrow-right"></i></a>
1271 </div>
1272 EOT;
1273         }
1274 
1275         $this->browse = $browse;
1276 
1277         # the hidden select is a trick for vertical alignement with the operation select
1278 
1279         return <<<EOT
1280 <div class="listview-controls">
1281     {$ranger}{$page_limit_selector}{$browse}
1282 </div>
1283 EOT;
1284     }
1285 
1286     /**
1287      * Renders jobs as an HTML element.
1288      *
1289      * @param array $jobs
1290      *
1291      * @return \Brickrouge\Element\null
1292      */
1293     protected function render_jobs(array $jobs)
1294     {
1295         if (!$jobs)
1296         {
1297             return;
1298         }
1299 
1300         $children = array();
1301 
1302         foreach ($jobs as $operation => $label)
1303         {
1304             $children[] = new Button($label, array('data-operation' => $operation, 'data-target' => 'manager'));
1305         }
1306 
1307         return new Element
1308         (
1309             'div', array
1310             (
1311                 Element::CHILDREN => array
1312                 (
1313                     '<i class="icon-warning-sign context-icon"></i>',
1314 
1315                     /*
1316                     new Element
1317                     (
1318                         'label', array
1319                         (
1320                             Element::INNER_HTML => "Pour la sélection&nbsp;:",
1321 
1322                             'class' => 'btn-group-label'
1323                         )
1324                     ),
1325                     */
1326 
1327                     new Element
1328                     (
1329                         'div', array
1330                         (
1331                             Element::CHILDREN => $children,
1332 
1333                             'class' => 'btn-group'
1334                         )
1335                     )
1336                 ),
1337 
1338                 'data-actionbar-context' => 'operations',
1339                 'class' => 'listview-operations inline'
1340             )
1341         );
1342     }
1343 
1344     /**
1345      * Renders the element's footer.
1346      *
1347      * @return string
1348      */
1349     protected function render_foot()
1350     {
1351         $ncolumns = count($this->columns);
1352         $key_column = $this->primary_key ? '<td class="key">&nbsp;</td>' : '';
1353         $rendered_jobs = null;
1354         $rendered_controls = $this->render_controls();
1355 
1356         return <<<EOT
1357 <tfoot>
1358     <tr>
1359         $key_column
1360         <td colspan="{$ncolumns}">{$rendered_jobs}{$rendered_controls}</td>
1361     </tr>
1362 </tfoot>
1363 EOT;
1364     }
1365 
1366     /**
1367      * Checks if the view is filtered.
1368      *
1369      * @param string $column_id This optional parameter can be used to check if the filter
1370      * is applied to a specific column.
1371      *
1372      * @return boolean
1373      */
1374     public function is_filtering($column_id=null)
1375     {
1376         return $this->options->is_filtering($column_id);
1377     }
1378 
1379     protected function get_is_filtering()
1380     {
1381         return $this->is_filtering();
1382     }
1383 }
1384 
1385 /*
1386  * Events
1387  */
1388 
1389 namespace Icybee\ManageBlock;
1390 
1391 use ICanBoogie\ActiveRecord\Query;
1392 use ICanBoogie\Event;
1393 
1394 use Icybee\ManageBlock;
1395 use Icybee\ManageBlock\Options;
1396 
1397 /**
1398  * Event class for the `Icybee\ManageBlock::register_columns` event.
1399  */
1400 class RegisterColumnsEvent extends Event
1401 {
1402     /**
1403      * Reference to the columns of the element.
1404      *
1405      * @var array[string]array
1406      */
1407     public $columns;
1408 
1409     /**
1410      * The event is constructed with the type `register_columns`.
1411      *
1412      * @param \Icybee\ManageBlock $target
1413      * @param array $columns Reference to the columns of the element.
1414      */
1415     public function __construct(ManageBlock $target, array &$columns)
1416     {
1417         $this->columns = &$columns;
1418 
1419         parent::__construct($target, 'register_columns');
1420     }
1421 
1422     public function add(Column $column, $weight=null)
1423     {
1424         if ($weight)
1425         {
1426             list($position, $relative) = explode(':', $weight) + array('before');
1427 
1428             $this->columns = \ICanBoogie\array_insert($this->columns, $relative, $column, $column->id, $position == 'after');
1429         }
1430         else
1431         {
1432             $this->columns[$column->id] = $column;
1433         }
1434     }
1435 }
1436 
1437 /**
1438  * Event class for the `Icybee\ManageBlock::alter_columns` event.
1439  */
1440 class AlterColumnsEvent extends Event
1441 {
1442     /**
1443      * Reference to the columns of the element.
1444      *
1445      * @var array[string]array
1446      */
1447     public $columns;
1448 
1449     /**
1450      * The event is constructed with the type `alter_columns`.
1451      *
1452      * @param \Icybee\ManageBlock $target
1453      * @param array $columns Reference to the columns of the element.
1454      */
1455     public function __construct(ManageBlock $target, array &$columns)
1456     {
1457         $this->columns = &$columns;
1458 
1459         parent::__construct($target, 'alter_columns');
1460     }
1461 
1462     public function add(Column $column, $weight=null)
1463     {
1464         if ($weight)
1465         {
1466             list($position, $relative) = explode(':', $weight) + array('before');
1467 
1468             $this->columns = \ICanBoogie\array_insert($this->columns, $relative, $column, $column->id, $position == 'after');
1469         }
1470         else
1471         {
1472             $this->columns[$column->id] = $column;
1473         }
1474     }
1475 }
1476 
1477 class AlterRenderedCellsEvent extends Event
1478 {
1479     /**
1480      * Reference to the rendered cells.
1481      *
1482      * @var array[string]string
1483      */
1484     public $rendered_cells;
1485 
1486     /**
1487      * The records used to render the cells.
1488      *
1489      * @var array[]\ICanBoogie\ActiveRecord
1490      */
1491     public $records;
1492 
1493     public function __construct(ManageBlock $target, array &$rendered_cells, array $records)
1494     {
1495         $this->rendered_cells = &$rendered_cells;
1496         $this->records = $records;
1497 
1498         parent::__construct($target, 'alter_rendered_cells');
1499     }
1500 }
1501 
1502 class AlterQueryEvent extends Event
1503 {
1504     public $query;
1505 
1506     public $options;
1507 
1508     public function __construct(ManageBlock $target, Query $query, Options $options)
1509     {
1510         $this->query = $query;
1511         $this->options = $options;
1512 
1513         parent::__construct($target, 'alter_query');
1514     }
1515 }
Autodoc API documentation generated by ApiGen 2.8.0