CakePHP
  • Documentation
    • Book
    • API
    • Videos
    • Logos & Trademarks
  • Business Solutions
  • Swag
  • Road Trip
  • Team
  • Community
    • Community
    • Team
    • Issues (Github)
    • YouTube Channel
    • Get Involved
    • Bakery
    • Featured Resources
    • Newsletter
    • Certification
    • My CakePHP
    • CakeFest
    • Facebook
    • Twitter
    • Help & Support
    • Forum
    • Stack Overflow
    • IRC
    • Slack
    • Paid Support
CakePHP

C CakePHP 3.8 Red Velvet API

  • Overview
  • Tree
  • Deprecated
  • Version:
    • 3.8
      • 3.8
      • 3.7
      • 3.6
      • 3.5
      • 3.4
      • 3.3
      • 3.2
      • 3.1
      • 3.0
      • 2.10
      • 2.9
      • 2.8
      • 2.7
      • 2.6
      • 2.5
      • 2.4
      • 2.3
      • 2.2
      • 2.1
      • 2.0
      • 1.3
      • 1.2

Namespaces

  • Cake
    • Auth
      • Storage
    • Cache
      • Engine
    • Collection
      • Iterator
    • Command
    • Console
      • Exception
    • Controller
      • Component
      • Exception
    • Core
      • Configure
        • Engine
      • Exception
      • Retry
    • Database
      • Driver
      • Exception
      • Expression
      • Schema
      • Statement
      • Type
    • Datasource
      • Exception
    • Error
      • Middleware
    • Event
      • Decorator
    • Filesystem
    • Form
    • Http
      • Client
        • Adapter
        • Auth
      • Cookie
      • Exception
      • Middleware
      • Session
    • I18n
      • Formatter
      • Middleware
      • Parser
    • Log
      • Engine
    • Mailer
      • Exception
      • Transport
    • Network
      • Exception
    • ORM
      • Association
      • Behavior
        • Translate
      • Exception
      • Locator
      • Rule
    • Routing
      • Exception
      • Filter
      • Middleware
      • Route
    • Shell
      • Helper
      • Task
    • TestSuite
      • Fixture
      • Stub
    • Utility
      • Exception
    • Validation
    • View
      • Exception
      • Form
      • Helper
      • Widget
  • None

Classes

  • ArrayContext
  • ContextFactory
  • EntityContext
  • FormContext
  • NullContext

Interfaces

  • ContextInterface
  1: <?php
  2: /**
  3:  * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  4:  * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  5:  *
  6:  * Licensed under The MIT License
  7:  * For full copyright and license information, please see the LICENSE.txt
  8:  * Redistributions of files must retain the above copyright notice.
  9:  *
 10:  * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 11:  * @link          https://cakephp.org CakePHP(tm) Project
 12:  * @since         3.0.0
 13:  * @license       https://opensource.org/licenses/mit-license.php MIT License
 14:  */
 15: namespace Cake\View\Form;
 16: 
 17: use ArrayAccess;
 18: use Cake\Collection\Collection;
 19: use Cake\Datasource\EntityInterface;
 20: use Cake\Datasource\RepositoryInterface;
 21: use Cake\Http\ServerRequest;
 22: use Cake\ORM\Locator\LocatorAwareTrait;
 23: use Cake\Utility\Inflector;
 24: use RuntimeException;
 25: use Traversable;
 26: 
 27: /**
 28:  * Provides a form context around a single entity and its relations.
 29:  * It also can be used as context around an array or iterator of entities.
 30:  *
 31:  * This class lets FormHelper interface with entities or collections
 32:  * of entities.
 33:  *
 34:  * Important Keys:
 35:  *
 36:  * - `entity` The entity this context is operating on.
 37:  * - `table` Either the ORM\Table instance to fetch schema/validators
 38:  *   from, an array of table instances in the case of a form spanning
 39:  *   multiple entities, or the name(s) of the table.
 40:  *   If this is null the table name(s) will be determined using naming
 41:  *   conventions.
 42:  * - `validator` Either the Validation\Validator to use, or the name of the
 43:  *   validation method to call on the table object. For example 'default'.
 44:  *   Defaults to 'default'. Can be an array of table alias=>validators when
 45:  *   dealing with associated forms.
 46:  */
 47: class EntityContext implements ContextInterface
 48: {
 49:     use LocatorAwareTrait;
 50: 
 51:     /**
 52:      * The request object.
 53:      *
 54:      * @var \Cake\Http\ServerRequest
 55:      */
 56:     protected $_request;
 57: 
 58:     /**
 59:      * Context data for this object.
 60:      *
 61:      * @var array
 62:      */
 63:     protected $_context;
 64: 
 65:     /**
 66:      * The name of the top level entity/table object.
 67:      *
 68:      * @var string
 69:      */
 70:     protected $_rootName;
 71: 
 72:     /**
 73:      * Boolean to track whether or not the entity is a
 74:      * collection.
 75:      *
 76:      * @var bool
 77:      */
 78:     protected $_isCollection = false;
 79: 
 80:     /**
 81:      * A dictionary of tables
 82:      *
 83:      * @var array
 84:      */
 85:     protected $_tables = [];
 86: 
 87:     /**
 88:      * Dictionary of validators.
 89:      *
 90:      * @var \Cake\Validation\Validator[]
 91:      */
 92:     protected $_validator = [];
 93: 
 94:     /**
 95:      * Constructor.
 96:      *
 97:      * @param \Cake\Http\ServerRequest $request The request object.
 98:      * @param array $context Context info.
 99:      */
100:     public function __construct(ServerRequest $request, array $context)
101:     {
102:         $this->_request = $request;
103:         $context += [
104:             'entity' => null,
105:             'table' => null,
106:             'validator' => [],
107:         ];
108:         $this->_context = $context;
109:         $this->_prepare();
110:     }
111: 
112:     /**
113:      * Prepare some additional data from the context.
114:      *
115:      * If the table option was provided to the constructor and it
116:      * was a string, TableLocator will be used to get the correct table instance.
117:      *
118:      * If an object is provided as the table option, it will be used as is.
119:      *
120:      * If no table option is provided, the table name will be derived based on
121:      * naming conventions. This inference will work with a number of common objects
122:      * like arrays, Collection objects and ResultSets.
123:      *
124:      * @return void
125:      * @throws \RuntimeException When a table object cannot be located/inferred.
126:      */
127:     protected function _prepare()
128:     {
129:         $table = $this->_context['table'];
130:         $entity = $this->_context['entity'];
131:         if (empty($table)) {
132:             if (is_array($entity) || $entity instanceof Traversable) {
133:                 foreach ($entity as $e) {
134:                     $entity = $e;
135:                     break;
136:                 }
137:             }
138:             $isEntity = $entity instanceof EntityInterface;
139: 
140:             if ($isEntity) {
141:                 $table = $entity->getSource();
142:             }
143:             if (!$table && $isEntity && get_class($entity) !== 'Cake\ORM\Entity') {
144:                 list(, $entityClass) = namespaceSplit(get_class($entity));
145:                 $table = Inflector::pluralize($entityClass);
146:             }
147:         }
148:         if (is_string($table)) {
149:             $table = $this->getTableLocator()->get($table);
150:         }
151: 
152:         if (!($table instanceof RepositoryInterface)) {
153:             throw new RuntimeException(
154:                 'Unable to find table class for current entity'
155:             );
156:         }
157:         $this->_isCollection = (
158:             is_array($entity) ||
159:             $entity instanceof Traversable
160:         );
161: 
162:         $alias = $this->_rootName = $table->getAlias();
163:         $this->_tables[$alias] = $table;
164:     }
165: 
166:     /**
167:      * Get the primary key data for the context.
168:      *
169:      * Gets the primary key columns from the root entity's schema.
170:      *
171:      * @return array
172:      */
173:     public function primaryKey()
174:     {
175:         return (array)$this->_tables[$this->_rootName]->getPrimaryKey();
176:     }
177: 
178:     /**
179:      * {@inheritDoc}
180:      */
181:     public function isPrimaryKey($field)
182:     {
183:         $parts = explode('.', $field);
184:         $table = $this->_getTable($parts);
185:         $primaryKey = (array)$table->getPrimaryKey();
186: 
187:         return in_array(array_pop($parts), $primaryKey);
188:     }
189: 
190:     /**
191:      * Check whether or not this form is a create or update.
192:      *
193:      * If the context is for a single entity, the entity's isNew() method will
194:      * be used. If isNew() returns null, a create operation will be assumed.
195:      *
196:      * If the context is for a collection or array the first object in the
197:      * collection will be used.
198:      *
199:      * @return bool
200:      */
201:     public function isCreate()
202:     {
203:         $entity = $this->_context['entity'];
204:         if (is_array($entity) || $entity instanceof Traversable) {
205:             foreach ($entity as $e) {
206:                 $entity = $e;
207:                 break;
208:             }
209:         }
210:         if ($entity instanceof EntityInterface) {
211:             return $entity->isNew() !== false;
212:         }
213: 
214:         return true;
215:     }
216: 
217:     /**
218:      * Get the value for a given path.
219:      *
220:      * Traverses the entity data and finds the value for $path.
221:      *
222:      * @param string $field The dot separated path to the value.
223:      * @param array $options Options:
224:      *   - `default`: Default value to return if no value found in request
225:      *     data or entity.
226:      *   - `schemaDefault`: Boolean indicating whether default value from table
227:      *     schema should be used if it's not explicitly provided.
228:      * @return mixed The value of the field or null on a miss.
229:      */
230:     public function val($field, $options = [])
231:     {
232:         $options += [
233:             'default' => null,
234:             'schemaDefault' => true
235:         ];
236: 
237:         $val = $this->_request->getData($field);
238:         if ($val !== null) {
239:             return $val;
240:         }
241:         if (empty($this->_context['entity'])) {
242:             return $options['default'];
243:         }
244:         $parts = explode('.', $field);
245:         $entity = $this->entity($parts);
246: 
247:         if (end($parts) === '_ids' && !empty($entity)) {
248:             return $this->_extractMultiple($entity, $parts);
249:         }
250: 
251:         if ($entity instanceof EntityInterface) {
252:             $part = end($parts);
253:             $val = $entity->get($part);
254:             if ($val !== null) {
255:                 return $val;
256:             }
257:             if ($options['default'] !== null
258:                 || !$options['schemaDefault']
259:                 || !$entity->isNew()
260:             ) {
261:                 return $options['default'];
262:             }
263: 
264:             return $this->_schemaDefault($parts);
265:         }
266:         if (is_array($entity) || $entity instanceof ArrayAccess) {
267:             $key = array_pop($parts);
268: 
269:             return isset($entity[$key]) ? $entity[$key] : $options['default'];
270:         }
271: 
272:         return null;
273:     }
274: 
275:     /**
276:      * Get default value from table schema for given entity field.
277:      *
278:      * @param array $parts Each one of the parts in a path for a field name
279:      * @return mixed
280:      */
281:     protected function _schemaDefault($parts)
282:     {
283:         $table = $this->_getTable($parts);
284:         if ($table === false) {
285:             return null;
286:         }
287:         $field = end($parts);
288:         $defaults = $table->getSchema()->defaultValues();
289:         if (!array_key_exists($field, $defaults)) {
290:             return null;
291:         }
292: 
293:         return $defaults[$field];
294:     }
295: 
296:     /**
297:      * Helper method used to extract all the primary key values out of an array, The
298:      * primary key column is guessed out of the provided $path array
299:      *
300:      * @param array|\Traversable $values The list from which to extract primary keys from
301:      * @param array $path Each one of the parts in a path for a field name
302:      * @return array|null
303:      */
304:     protected function _extractMultiple($values, $path)
305:     {
306:         if (!(is_array($values) || $values instanceof Traversable)) {
307:             return null;
308:         }
309:         $table = $this->_getTable($path, false);
310:         $primary = $table ? (array)$table->getPrimaryKey() : ['id'];
311: 
312:         return (new Collection($values))->extract($primary[0])->toArray();
313:     }
314: 
315:     /**
316:      * Fetch the entity or data value for a given path
317:      *
318:      * This method will traverse the given path and find the entity
319:      * or array value for a given path.
320:      *
321:      * If you only want the terminal Entity for a path use `leafEntity` instead.
322:      *
323:      * @param array|null $path Each one of the parts in a path for a field name
324:      *  or null to get the entity passed in constructor context.
325:      * @return \Cake\Datasource\EntityInterface|\Traversable|array|bool
326:      * @throws \RuntimeException When properties cannot be read.
327:      */
328:     public function entity($path = null)
329:     {
330:         if ($path === null) {
331:             return $this->_context['entity'];
332:         }
333: 
334:         $oneElement = count($path) === 1;
335:         if ($oneElement && $this->_isCollection) {
336:             return false;
337:         }
338:         $entity = $this->_context['entity'];
339:         if ($oneElement) {
340:             return $entity;
341:         }
342: 
343:         if ($path[0] === $this->_rootName) {
344:             $path = array_slice($path, 1);
345:         }
346: 
347:         $len = count($path);
348:         $last = $len - 1;
349:         for ($i = 0; $i < $len; $i++) {
350:             $prop = $path[$i];
351:             $next = $this->_getProp($entity, $prop);
352:             $isLast = ($i === $last);
353:             if (!$isLast && $next === null && $prop !== '_ids') {
354:                 $table = $this->_getTable($path);
355: 
356:                 return $table->newEntity();
357:             }
358: 
359:             $isTraversable = (
360:                 is_array($next) ||
361:                 $next instanceof Traversable ||
362:                 $next instanceof EntityInterface
363:             );
364:             if ($isLast || !$isTraversable) {
365:                 return $entity;
366:             }
367:             $entity = $next;
368:         }
369:         throw new RuntimeException(sprintf(
370:             'Unable to fetch property "%s"',
371:             implode('.', $path)
372:         ));
373:     }
374: 
375:     /**
376:      * Fetch the terminal or leaf entity for the given path.
377:      *
378:      * Traverse the path until an entity cannot be found. Lists containing
379:      * entities will be traversed if the first element contains an entity.
380:      * Otherwise the containing Entity will be assumed to be the terminal one.
381:      *
382:      * @param array|null $path Each one of the parts in a path for a field name
383:      *  or null to get the entity passed in constructor context.
384:      * @return array Containing the found entity, and remaining un-matched path.
385:      * @throws \RuntimeException When properties cannot be read.
386:      */
387:     protected function leafEntity($path = null)
388:     {
389:         if ($path === null) {
390:             return $this->_context['entity'];
391:         }
392: 
393:         $oneElement = count($path) === 1;
394:         if ($oneElement && $this->_isCollection) {
395:             throw new RuntimeException(sprintf(
396:                 'Unable to fetch property "%s"',
397:                 implode('.', $path)
398:             ));
399:         }
400:         $entity = $this->_context['entity'];
401:         if ($oneElement) {
402:             return [$entity, $path];
403:         }
404: 
405:         if ($path[0] === $this->_rootName) {
406:             $path = array_slice($path, 1);
407:         }
408: 
409:         $len = count($path);
410:         $last = $len - 1;
411:         $leafEntity = $entity;
412:         for ($i = 0; $i < $len; $i++) {
413:             $prop = $path[$i];
414:             $next = $this->_getProp($entity, $prop);
415: 
416:             // Did not dig into an entity, return the current one.
417:             if (is_array($entity) && !($next instanceof EntityInterface || $next instanceof Traversable)) {
418:                 return [$leafEntity, array_slice($path, $i - 1)];
419:             }
420: 
421:             if ($next instanceof EntityInterface) {
422:                 $leafEntity = $next;
423:             }
424: 
425:             // If we are at the end of traversable elements
426:             // return the last entity found.
427:             $isTraversable = (
428:                 is_array($next) ||
429:                 $next instanceof Traversable ||
430:                 $next instanceof EntityInterface
431:             );
432:             if (!$isTraversable) {
433:                 return [$leafEntity, array_slice($path, $i)];
434:             }
435:             $entity = $next;
436:         }
437:         throw new RuntimeException(sprintf(
438:             'Unable to fetch property "%s"',
439:             implode('.', $path)
440:         ));
441:     }
442: 
443:     /**
444:      * Read property values or traverse arrays/iterators.
445:      *
446:      * @param mixed $target The entity/array/collection to fetch $field from.
447:      * @param string $field The next field to fetch.
448:      * @return mixed
449:      */
450:     protected function _getProp($target, $field)
451:     {
452:         if (is_array($target) && isset($target[$field])) {
453:             return $target[$field];
454:         }
455:         if ($target instanceof EntityInterface) {
456:             return $target->get($field);
457:         }
458:         if ($target instanceof Traversable) {
459:             foreach ($target as $i => $val) {
460:                 if ($i == $field) {
461:                     return $val;
462:                 }
463:             }
464: 
465:             return false;
466:         }
467:     }
468: 
469:     /**
470:      * Check if a field should be marked as required.
471:      *
472:      * @param string $field The dot separated path to the field you want to check.
473:      * @return bool
474:      */
475:     public function isRequired($field)
476:     {
477:         $parts = explode('.', $field);
478:         $entity = $this->entity($parts);
479: 
480:         $isNew = true;
481:         if ($entity instanceof EntityInterface) {
482:             $isNew = $entity->isNew();
483:         }
484: 
485:         $validator = $this->_getValidator($parts);
486:         $fieldName = array_pop($parts);
487:         if (!$validator->hasField($fieldName)) {
488:             return false;
489:         }
490:         if ($this->type($field) !== 'boolean') {
491:             return $validator->isEmptyAllowed($fieldName, $isNew) === false;
492:         }
493: 
494:         return false;
495:     }
496: 
497:     /**
498:      * {@inheritDoc}
499:      */
500:     public function getRequiredMessage($field)
501:     {
502:         $parts = explode('.', $field);
503: 
504:         $validator = $this->_getValidator($parts);
505:         $fieldName = array_pop($parts);
506:         if (!$validator->hasField($fieldName)) {
507:             return null;
508:         }
509: 
510:         $ruleset = $validator->field($fieldName);
511: 
512:         $requiredMessage = $validator->getRequiredMessage($fieldName);
513:         $emptyMessage = $validator->getNotEmptyMessage($fieldName);
514: 
515:         if ($ruleset->isPresenceRequired() && $requiredMessage) {
516:             return $requiredMessage;
517:         }
518:         if (!$ruleset->isEmptyAllowed() && $emptyMessage) {
519:             return $emptyMessage;
520:         }
521: 
522:         return null;
523:     }
524: 
525:     /**
526:      * Get field length from validation
527:      *
528:      * @param string $field The dot separated path to the field you want to check.
529:      * @return int|null
530:      */
531:     public function getMaxLength($field)
532:     {
533:         $parts = explode('.', $field);
534:         $validator = $this->_getValidator($parts);
535:         $fieldName = array_pop($parts);
536:         if (!$validator->hasField($fieldName)) {
537:             return null;
538:         }
539:         foreach ($validator->field($fieldName)->rules() as $rule) {
540:             if ($rule->get('rule') === 'maxLength') {
541:                 return $rule->get('pass')[0];
542:             }
543:         }
544: 
545:         return null;
546:     }
547: 
548:     /**
549:      * Get the field names from the top level entity.
550:      *
551:      * If the context is for an array of entities, the 0th index will be used.
552:      *
553:      * @return array Array of fieldnames in the table/entity.
554:      */
555:     public function fieldNames()
556:     {
557:         $table = $this->_getTable('0');
558: 
559:         return $table->getSchema()->columns();
560:     }
561: 
562:     /**
563:      * Get the validator associated to an entity based on naming
564:      * conventions.
565:      *
566:      * @param array $parts Each one of the parts in a path for a field name
567:      * @return \Cake\Validation\Validator
568:      */
569:     protected function _getValidator($parts)
570:     {
571:         $keyParts = array_filter(array_slice($parts, 0, -1), function ($part) {
572:             return !is_numeric($part);
573:         });
574:         $key = implode('.', $keyParts);
575:         $entity = $this->entity($parts) ?: null;
576: 
577:         if (isset($this->_validator[$key])) {
578:             $this->_validator[$key]->setProvider('entity', $entity);
579: 
580:             return $this->_validator[$key];
581:         }
582: 
583:         $table = $this->_getTable($parts);
584:         $alias = $table->getAlias();
585: 
586:         $method = 'default';
587:         if (is_string($this->_context['validator'])) {
588:             $method = $this->_context['validator'];
589:         } elseif (isset($this->_context['validator'][$alias])) {
590:             $method = $this->_context['validator'][$alias];
591:         }
592: 
593:         $validator = $table->getValidator($method);
594:         $validator->setProvider('entity', $entity);
595: 
596:         return $this->_validator[$key] = $validator;
597:     }
598: 
599:     /**
600:      * Get the table instance from a property path
601:      *
602:      * @param array $parts Each one of the parts in a path for a field name
603:      * @param bool $fallback Whether or not to fallback to the last found table
604:      *  when a non-existent field/property is being encountered.
605:      * @return \Cake\ORM\Table|bool Table instance or false
606:      */
607:     protected function _getTable($parts, $fallback = true)
608:     {
609:         if (!is_array($parts) || count($parts) === 1) {
610:             return $this->_tables[$this->_rootName];
611:         }
612: 
613:         $normalized = array_slice(array_filter($parts, function ($part) {
614:             return !is_numeric($part);
615:         }), 0, -1);
616: 
617:         $path = implode('.', $normalized);
618:         if (isset($this->_tables[$path])) {
619:             return $this->_tables[$path];
620:         }
621: 
622:         if (current($normalized) === $this->_rootName) {
623:             $normalized = array_slice($normalized, 1);
624:         }
625: 
626:         $table = $this->_tables[$this->_rootName];
627:         $assoc = null;
628:         foreach ($normalized as $part) {
629:             if ($part === '_joinData') {
630:                 if ($assoc) {
631:                     $table = $assoc->junction();
632:                     $assoc = null;
633:                     continue;
634:                 }
635:             } else {
636:                 $assoc = $table->associations()->getByProperty($part);
637:             }
638: 
639:             if (!$assoc && $fallback) {
640:                 break;
641:             }
642:             if (!$assoc && !$fallback) {
643:                 return false;
644:             }
645: 
646:             $table = $assoc->getTarget();
647:         }
648: 
649:         return $this->_tables[$path] = $table;
650:     }
651: 
652:     /**
653:      * Get the abstract field type for a given field name.
654:      *
655:      * @param string $field A dot separated path to get a schema type for.
656:      * @return string|null An abstract data type or null.
657:      * @see \Cake\Database\Type
658:      */
659:     public function type($field)
660:     {
661:         $parts = explode('.', $field);
662:         $table = $this->_getTable($parts);
663: 
664:         return $table->getSchema()->baseColumnType(array_pop($parts));
665:     }
666: 
667:     /**
668:      * Get an associative array of other attributes for a field name.
669:      *
670:      * @param string $field A dot separated path to get additional data on.
671:      * @return array An array of data describing the additional attributes on a field.
672:      */
673:     public function attributes($field)
674:     {
675:         $parts = explode('.', $field);
676:         $table = $this->_getTable($parts);
677:         $column = (array)$table->getSchema()->getColumn(array_pop($parts));
678:         $whitelist = ['length' => null, 'precision' => null];
679: 
680:         return array_intersect_key($column, $whitelist);
681:     }
682: 
683:     /**
684:      * Check whether or not a field has an error attached to it
685:      *
686:      * @param string $field A dot separated path to check errors on.
687:      * @return bool Returns true if the errors for the field are not empty.
688:      */
689:     public function hasError($field)
690:     {
691:         return $this->error($field) !== [];
692:     }
693: 
694:     /**
695:      * Get the errors for a given field
696:      *
697:      * @param string $field A dot separated path to check errors on.
698:      * @return array An array of errors.
699:      */
700:     public function error($field)
701:     {
702:         $parts = explode('.', $field);
703:         try {
704:             list($entity, $remainingParts) = $this->leafEntity($parts);
705:         } catch (RuntimeException $e) {
706:             return [];
707:         }
708:         if (count($remainingParts) === 0) {
709:             return $entity->getErrors();
710:         }
711: 
712:         if ($entity instanceof EntityInterface) {
713:             $error = $entity->getError(implode('.', $remainingParts));
714:             if ($error) {
715:                 return $error;
716:             }
717: 
718:             return $entity->getError(array_pop($parts));
719:         }
720: 
721:         return [];
722:     }
723: }
724: 
Follow @CakePHP
#IRC
OpenHub
Rackspace
  • Business Solutions
  • Showcase
  • Documentation
  • Book
  • API
  • Videos
  • Logos & Trademarks
  • Community
  • Team
  • Issues (Github)
  • YouTube Channel
  • Get Involved
  • Bakery
  • Featured Resources
  • Newsletter
  • Certification
  • My CakePHP
  • CakeFest
  • Facebook
  • Twitter
  • Help & Support
  • Forum
  • Stack Overflow
  • IRC
  • Slack
  • Paid Support

Generated using CakePHP API Docs