EntityContextTest.php 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Test\TestCase\View\Form;
  17. use ArrayIterator;
  18. use ArrayObject;
  19. use Cake\Collection\Collection;
  20. use Cake\ORM\Entity;
  21. use Cake\TestSuite\TestCase;
  22. use Cake\Validation\Validator;
  23. use Cake\View\Form\EntityContext;
  24. use RuntimeException;
  25. use stdClass;
  26. use TestApp\Model\Entity\Article;
  27. use TestApp\Model\Entity\ArticlesTag;
  28. use TestApp\Model\Entity\Tag;
  29. /**
  30. * Entity context test case.
  31. */
  32. class EntityContextTest extends TestCase
  33. {
  34. /**
  35. * Fixtures to use.
  36. *
  37. * @var array<string>
  38. */
  39. protected array $fixtures = ['core.Articles', 'core.Comments', 'core.Tags', 'core.ArticlesTags'];
  40. /**
  41. * tests getRequiredMessage
  42. */
  43. public function testGetRequiredMessage(): void
  44. {
  45. $this->_setupTables();
  46. $context = new EntityContext([
  47. 'entity' => new Article(),
  48. 'table' => 'Articles',
  49. 'validator' => 'create',
  50. ]);
  51. $this->assertNull($context->getRequiredMessage('body'));
  52. $this->assertSame('Don\'t forget a title!', $context->getRequiredMessage('title'));
  53. }
  54. /**
  55. * Test getting entity back from context.
  56. */
  57. public function testEntity(): void
  58. {
  59. $row = new Article();
  60. $context = new EntityContext([
  61. 'entity' => $row,
  62. ]);
  63. $this->assertSame($row, $context->entity());
  64. }
  65. /**
  66. * Test getting primary key data.
  67. */
  68. public function testPrimaryKey(): void
  69. {
  70. $row = new Article();
  71. $context = new EntityContext([
  72. 'entity' => $row,
  73. ]);
  74. $this->assertEquals(['id'], $context->getPrimaryKey());
  75. }
  76. /**
  77. * Test isPrimaryKey
  78. */
  79. public function testIsPrimaryKey(): void
  80. {
  81. $this->_setupTables();
  82. $row = new Article();
  83. $context = new EntityContext([
  84. 'entity' => $row,
  85. ]);
  86. $this->assertTrue($context->isPrimaryKey('id'));
  87. $this->assertFalse($context->isPrimaryKey('title'));
  88. $this->assertTrue($context->isPrimaryKey('1.id'));
  89. $this->assertTrue($context->isPrimaryKey('Articles.1.id'));
  90. $this->assertTrue($context->isPrimaryKey('comments.0.id'));
  91. $this->assertTrue($context->isPrimaryKey('1.comments.0.id'));
  92. $this->assertFalse($context->isPrimaryKey('1.comments.0.comment'));
  93. $this->assertFalse($context->isPrimaryKey('Articles.1.comments.0.comment'));
  94. $this->assertTrue($context->isPrimaryKey('tags.0._joinData.article_id'));
  95. $this->assertTrue($context->isPrimaryKey('tags.0._joinData.tag_id'));
  96. }
  97. /**
  98. * Test isCreate on a single entity.
  99. */
  100. public function testIsCreateSingle(): void
  101. {
  102. $row = new Article();
  103. $context = new EntityContext([
  104. 'entity' => $row,
  105. ]);
  106. $this->assertTrue($context->isCreate());
  107. $row->setNew(false);
  108. $this->assertFalse($context->isCreate());
  109. $row->setNew(true);
  110. $this->assertTrue($context->isCreate());
  111. }
  112. /**
  113. * Test isCreate on a collection.
  114. *
  115. * @dataProvider collectionProvider
  116. * @param mixed $collection
  117. */
  118. public function testIsCreateCollection($collection): void
  119. {
  120. $context = new EntityContext([
  121. 'entity' => $collection,
  122. ]);
  123. $this->assertTrue($context->isCreate());
  124. }
  125. /**
  126. * Test an invalid table scope throws an error.
  127. */
  128. public function testInvalidTable(): void
  129. {
  130. $this->expectException(RuntimeException::class);
  131. $this->expectExceptionMessage('Unable to find table class for current entity');
  132. $row = new stdClass();
  133. $context = new EntityContext([
  134. 'entity' => $row,
  135. ]);
  136. }
  137. /**
  138. * Tests that passing a plain entity will give an error as it cannot be matched
  139. */
  140. public function testDefaultEntityError(): void
  141. {
  142. $this->expectException(RuntimeException::class);
  143. $this->expectExceptionMessage('Unable to find table class for current entity');
  144. $context = new EntityContext([
  145. 'entity' => new Entity(),
  146. ]);
  147. }
  148. /**
  149. * Tests that the table can be derived from the entity source if it is present
  150. */
  151. public function testTableFromEntitySource(): void
  152. {
  153. $entity = new Entity();
  154. $entity->setSource('Articles');
  155. $context = new EntityContext([
  156. 'entity' => $entity,
  157. ]);
  158. $expected = ['id', 'author_id', 'title', 'body', 'published'];
  159. $this->assertEquals($expected, $context->fieldNames());
  160. }
  161. /**
  162. * Test operations with no entity.
  163. */
  164. public function testOperationsNoEntity(): void
  165. {
  166. $context = new EntityContext([
  167. 'table' => 'Articles',
  168. ]);
  169. $this->assertNull($context->val('title'));
  170. $this->assertNull($context->isRequired('title'));
  171. $this->assertFalse($context->hasError('title'));
  172. $this->assertSame('string', $context->type('title'));
  173. $this->assertEquals([], $context->error('title'));
  174. $attrs = $context->attributes('title');
  175. $this->assertArrayHasKey('length', $attrs);
  176. $this->assertArrayHasKey('precision', $attrs);
  177. }
  178. /**
  179. * Test operations that lack a table argument.
  180. */
  181. public function testOperationsNoTableArg(): void
  182. {
  183. $row = new Article([
  184. 'title' => 'Test entity',
  185. 'body' => 'Something new',
  186. ]);
  187. $row->setError('title', ['Title is required.']);
  188. $context = new EntityContext([
  189. 'entity' => $row,
  190. ]);
  191. $result = $context->val('title');
  192. $this->assertEquals($row->title, $result);
  193. $result = $context->error('title');
  194. $this->assertEquals($row->getError('title'), $result);
  195. $this->assertTrue($context->hasError('title'));
  196. }
  197. /**
  198. * Test collection operations that lack a table argument.
  199. *
  200. * @dataProvider collectionProvider
  201. * @param mixed $collection
  202. */
  203. public function testCollectionOperationsNoTableArg($collection): void
  204. {
  205. $context = new EntityContext([
  206. 'entity' => $collection,
  207. ]);
  208. $result = $context->val('0.title');
  209. $this->assertSame('First post', $result);
  210. $result = $context->error('1.body');
  211. $this->assertEquals(['Not long enough'], $result);
  212. $this->assertNull($context->val('0'));
  213. }
  214. /**
  215. * Data provider for testing collections.
  216. *
  217. * @return array
  218. */
  219. public static function collectionProvider(): array
  220. {
  221. $one = new Article([
  222. 'title' => 'First post',
  223. 'body' => 'Stuff',
  224. 'user' => new Entity(['username' => 'mark']),
  225. ]);
  226. $one->setError('title', 'Required field');
  227. $two = new Article([
  228. 'title' => 'Second post',
  229. 'body' => 'Some text',
  230. 'user' => new Entity(['username' => 'jose']),
  231. ]);
  232. $two->setError('body', 'Not long enough');
  233. return [
  234. 'array' => [[$one, $two]],
  235. 'basic iterator' => [new ArrayObject([$one, $two])],
  236. 'array iterator' => [new ArrayIterator([$one, $two])],
  237. 'collection' => [new Collection([$one, $two])],
  238. ];
  239. }
  240. /**
  241. * Test operations on a collection of entities.
  242. *
  243. * @dataProvider collectionProvider
  244. * @param mixed $collection
  245. */
  246. public function testValOnCollections($collection): void
  247. {
  248. $context = new EntityContext([
  249. 'entity' => $collection,
  250. 'table' => 'Articles',
  251. ]);
  252. $result = $context->val('0.title');
  253. $this->assertSame('First post', $result);
  254. $result = $context->val('0.user.username');
  255. $this->assertSame('mark', $result);
  256. $result = $context->val('1.title');
  257. $this->assertSame('Second post', $result);
  258. $result = $context->val('1.user.username');
  259. $this->assertSame('jose', $result);
  260. $this->assertNull($context->val('nope'));
  261. $this->assertNull($context->val('99.title'));
  262. }
  263. /**
  264. * Test operations on a collection of entities when prefixing with the
  265. * table name
  266. *
  267. * @dataProvider collectionProvider
  268. * @param mixed $collection
  269. */
  270. public function testValOnCollectionsWithRootName($collection): void
  271. {
  272. $context = new EntityContext([
  273. 'entity' => $collection,
  274. 'table' => 'Articles',
  275. ]);
  276. $result = $context->val('Articles.0.title');
  277. $this->assertSame('First post', $result);
  278. $result = $context->val('Articles.0.user.username');
  279. $this->assertSame('mark', $result);
  280. $result = $context->val('Articles.1.title');
  281. $this->assertSame('Second post', $result);
  282. $result = $context->val('Articles.1.user.username');
  283. $this->assertSame('jose', $result);
  284. $this->assertNull($context->val('Articles.99.title'));
  285. }
  286. /**
  287. * Test error operations on a collection of entities.
  288. *
  289. * @dataProvider collectionProvider
  290. * @param mixed $collection
  291. */
  292. public function testErrorsOnCollections($collection): void
  293. {
  294. $context = new EntityContext([
  295. 'entity' => $collection,
  296. 'table' => 'Articles',
  297. ]);
  298. $this->assertTrue($context->hasError('0.title'));
  299. $this->assertEquals(['Required field'], $context->error('0.title'));
  300. $this->assertFalse($context->hasError('0.body'));
  301. $this->assertFalse($context->hasError('1.title'));
  302. $this->assertEquals(['Not long enough'], $context->error('1.body'));
  303. $this->assertTrue($context->hasError('1.body'));
  304. $this->assertFalse($context->hasError('nope'));
  305. $this->assertFalse($context->hasError('99.title'));
  306. }
  307. /**
  308. * Test schema operations on a collection of entities.
  309. *
  310. * @dataProvider collectionProvider
  311. * @param mixed $collection
  312. */
  313. public function testSchemaOnCollections($collection): void
  314. {
  315. $this->_setupTables();
  316. $context = new EntityContext([
  317. 'entity' => $collection,
  318. 'table' => 'Articles',
  319. ]);
  320. $this->assertSame('string', $context->type('0.title'));
  321. $this->assertSame('text', $context->type('1.body'));
  322. $this->assertSame('string', $context->type('0.user.username'));
  323. $this->assertSame('string', $context->type('1.user.username'));
  324. $this->assertSame('string', $context->type('99.title'));
  325. $this->assertNull($context->type('0.nope'));
  326. $expected = [
  327. 'length' => 255, 'precision' => null,
  328. 'null' => null, 'default' => null, 'comment' => null,
  329. ];
  330. $this->assertEquals($expected, $context->attributes('0.user.username'));
  331. }
  332. /**
  333. * Test validation operations on a collection of entities.
  334. *
  335. * @dataProvider collectionProvider
  336. * @param mixed $collection
  337. */
  338. public function testValidatorsOnCollections($collection): void
  339. {
  340. $this->_setupTables();
  341. $context = new EntityContext([
  342. 'entity' => $collection,
  343. 'table' => 'Articles',
  344. 'validator' => [
  345. 'Articles' => 'create',
  346. 'Users' => 'custom',
  347. ],
  348. ]);
  349. $this->assertNull($context->isRequired('nope'));
  350. $this->assertTrue($context->isRequired('0.title'));
  351. $this->assertTrue($context->isRequired('0.user.username'));
  352. $this->assertFalse($context->isRequired('1.body'));
  353. $this->assertTrue($context->isRequired('99.title'));
  354. $this->assertNull($context->isRequired('99.nope'));
  355. }
  356. /**
  357. * Test reading data.
  358. */
  359. public function testValBasic(): void
  360. {
  361. $row = new Article([
  362. 'title' => 'Test entity',
  363. 'body' => 'Something new',
  364. ]);
  365. $context = new EntityContext([
  366. 'entity' => $row,
  367. 'table' => 'Articles',
  368. ]);
  369. $result = $context->val('title');
  370. $this->assertEquals($row->title, $result);
  371. $result = $context->val('body');
  372. $this->assertEquals($row->body, $result);
  373. $result = $context->val('nope');
  374. $this->assertNull($result);
  375. }
  376. /**
  377. * Test reading invalid data.
  378. */
  379. public function testValInvalid(): void
  380. {
  381. $row = new Article([
  382. 'title' => 'Valid title',
  383. ]);
  384. $row->setInvalidField('title', 'Invalid title');
  385. $context = new EntityContext([
  386. 'entity' => $row,
  387. 'table' => 'Articles',
  388. ]);
  389. $result = $context->val('title');
  390. $this->assertSame('Invalid title', $result);
  391. }
  392. /**
  393. * Test default values when entity is an array.
  394. */
  395. public function testValDefaultArray(): void
  396. {
  397. $context = new EntityContext([
  398. 'entity' => new Article([
  399. 'prop' => ['title' => 'foo'],
  400. ]),
  401. 'table' => 'Articles',
  402. ]);
  403. $this->assertSame('foo', $context->val('prop.title', ['default' => 'bar']));
  404. $this->assertSame('bar', $context->val('prop.nope', ['default' => 'bar']));
  405. }
  406. /**
  407. * Test reading array values from an entity.
  408. */
  409. public function testValGetArrayValue(): void
  410. {
  411. $row = new Article([
  412. 'title' => 'Test entity',
  413. 'types' => [1, 2, 3],
  414. 'tag' => [
  415. 'name' => 'Test tag',
  416. ],
  417. 'author' => new Entity([
  418. 'roles' => ['admin', 'publisher'],
  419. 'aliases' => new ArrayObject(['dave', 'david']),
  420. ]),
  421. ]);
  422. $context = new EntityContext([
  423. 'entity' => $row,
  424. 'table' => 'Articles',
  425. ]);
  426. $result = $context->val('types');
  427. $this->assertEquals($row->types, $result);
  428. $result = $context->val('author.roles');
  429. $this->assertEquals($row->author->roles, $result);
  430. $result = $context->val('tag.name');
  431. $this->assertEquals($row->tag['name'], $result);
  432. $result = $context->val('author.aliases.0');
  433. $this->assertEquals($row->author->aliases[0], $result, 'ArrayAccess can be read');
  434. $this->assertNull($context->val('author.aliases.3'));
  435. $this->assertNull($context->val('tag.nope'));
  436. $this->assertNull($context->val('author.roles.3'));
  437. }
  438. /**
  439. * Test reading values from associated entities.
  440. */
  441. public function testValAssociated(): void
  442. {
  443. $row = new Article([
  444. 'title' => 'Test entity',
  445. 'user' => new Entity([
  446. 'username' => 'mark',
  447. 'fname' => 'Mark',
  448. ]),
  449. 'comments' => [
  450. new Entity(['comment' => 'Test comment']),
  451. new Entity(['comment' => 'Second comment']),
  452. ],
  453. ]);
  454. $context = new EntityContext([
  455. 'entity' => $row,
  456. 'table' => 'Articles',
  457. ]);
  458. $result = $context->val('user.fname');
  459. $this->assertEquals($row->user->fname, $result);
  460. $result = $context->val('comments.0.comment');
  461. $this->assertEquals($row->comments[0]->comment, $result);
  462. $result = $context->val('comments.1.comment');
  463. $this->assertEquals($row->comments[1]->comment, $result);
  464. $result = $context->val('comments.0.nope');
  465. $this->assertNull($result);
  466. $result = $context->val('comments.0.nope.no_way');
  467. $this->assertNull($result);
  468. }
  469. /**
  470. * Tests that trying to get values from missing associations returns null
  471. */
  472. public function testValMissingAssociation(): void
  473. {
  474. $row = new Article([
  475. 'id' => 1,
  476. ]);
  477. $context = new EntityContext([
  478. 'entity' => $row,
  479. 'table' => 'Articles',
  480. ]);
  481. $result = $context->val('id');
  482. $this->assertEquals($row->id, $result);
  483. $this->assertNull($context->val('profile.id'));
  484. }
  485. /**
  486. * Test reading values from associated entities.
  487. */
  488. public function testValAssociatedHasMany(): void
  489. {
  490. $row = new Article([
  491. 'title' => 'First post',
  492. 'user' => new Entity([
  493. 'username' => 'mark',
  494. 'fname' => 'Mark',
  495. 'articles' => [
  496. new Article(['title' => 'First post']),
  497. new Article(['title' => 'Second post']),
  498. ],
  499. ]),
  500. ]);
  501. $context = new EntityContext([
  502. 'entity' => $row,
  503. 'table' => 'Articles',
  504. ]);
  505. $result = $context->val('user.articles.0.title');
  506. $this->assertSame('First post', $result);
  507. $result = $context->val('user.articles.1.title');
  508. $this->assertSame('Second post', $result);
  509. }
  510. /**
  511. * Test reading values for magic _ids input
  512. */
  513. public function testValAssociatedDefaultIds(): void
  514. {
  515. $row = new Article([
  516. 'title' => 'First post',
  517. 'user' => new Entity([
  518. 'username' => 'mark',
  519. 'fname' => 'Mark',
  520. 'sections' => [
  521. new Entity(['title' => 'PHP', 'id' => 1]),
  522. new Entity(['title' => 'Javascript', 'id' => 2]),
  523. ],
  524. ]),
  525. ]);
  526. $context = new EntityContext([
  527. 'entity' => $row,
  528. 'table' => 'Articles',
  529. ]);
  530. $result = $context->val('user.sections._ids');
  531. $this->assertEquals([1, 2], $result);
  532. }
  533. /**
  534. * Test reading values for magic _ids input
  535. */
  536. public function testValAssociatedCustomIds(): void
  537. {
  538. $this->_setupTables();
  539. $row = new Article([
  540. 'title' => 'First post',
  541. 'user' => new Entity([
  542. 'username' => 'mark',
  543. 'fname' => 'Mark',
  544. 'sections' => [
  545. new Entity(['title' => 'PHP', 'thing' => 1]),
  546. new Entity(['title' => 'Javascript', 'thing' => 4]),
  547. ],
  548. ]),
  549. ]);
  550. $context = new EntityContext([
  551. 'entity' => $row,
  552. 'table' => 'Articles',
  553. ]);
  554. $this->getTableLocator()->get('Users')->belongsToMany('Sections');
  555. $this->getTableLocator()->get('Sections')->setPrimaryKey('thing');
  556. $result = $context->val('user.sections._ids');
  557. $this->assertEquals([1, 4], $result);
  558. }
  559. /**
  560. * Test getting default value from table schema.
  561. */
  562. public function testValSchemaDefault(): void
  563. {
  564. $table = $this->getTableLocator()->get('Articles');
  565. $column = $table->getSchema()->getColumn('title');
  566. $table->getSchema()->addColumn('title', ['default' => 'default title'] + $column);
  567. $row = $table->newEmptyEntity();
  568. $context = new EntityContext([
  569. 'entity' => $row,
  570. 'table' => 'Articles',
  571. ]);
  572. $result = $context->val('title');
  573. $this->assertSame('default title', $result);
  574. }
  575. /**
  576. * Test getting association default value from table schema.
  577. */
  578. public function testValAssociatedSchemaDefault(): void
  579. {
  580. $table = $this->getTableLocator()->get('Articles');
  581. $associatedTable = $table->hasMany('Comments')->getTarget();
  582. $column = $associatedTable->getSchema()->getColumn('comment');
  583. $associatedTable->getSchema()->addColumn('comment', ['default' => 'default comment'] + $column);
  584. $row = $table->newEmptyEntity();
  585. $context = new EntityContext([
  586. 'entity' => $row,
  587. 'table' => 'Articles',
  588. ]);
  589. $result = $context->val('comments.0.comment');
  590. $this->assertSame('default comment', $result);
  591. }
  592. /**
  593. * Test getting association join table default value from table schema.
  594. */
  595. public function testValAssociatedJoinTableSchemaDefault(): void
  596. {
  597. $table = $this->getTableLocator()->get('Articles');
  598. $joinTable = $table
  599. ->belongsToMany('Tags')
  600. ->setThrough('ArticlesTags')
  601. ->junction();
  602. $joinTable->getSchema()->addColumn('column', [
  603. 'default' => 'default join table column value',
  604. 'type' => 'text',
  605. ]);
  606. $row = $table->newEmptyEntity();
  607. $context = new EntityContext([
  608. 'entity' => $row,
  609. 'table' => 'Articles',
  610. ]);
  611. $result = $context->val('tags.0._joinData.column');
  612. $this->assertSame('default join table column value', $result);
  613. }
  614. /**
  615. * Test validator for boolean fields.
  616. */
  617. public function testIsRequiredBooleanField(): void
  618. {
  619. $this->_setupTables();
  620. $context = new EntityContext([
  621. 'entity' => new Entity(),
  622. 'table' => 'Articles',
  623. ]);
  624. $articles = $this->getTableLocator()->get('Articles');
  625. $articles->getSchema()->addColumn('comments_on', [
  626. 'type' => 'boolean',
  627. ]);
  628. $validator = $articles->getValidator();
  629. $validator->add('comments_on', 'is_bool', [
  630. 'rule' => 'boolean',
  631. ]);
  632. $articles->setValidator('default', $validator);
  633. $this->assertNull($context->isRequired('title'));
  634. }
  635. /**
  636. * Test validator as a string.
  637. */
  638. public function testIsRequiredStringValidator(): void
  639. {
  640. $this->_setupTables();
  641. $context = new EntityContext([
  642. 'entity' => new Entity(),
  643. 'table' => 'Articles',
  644. 'validator' => 'create',
  645. ]);
  646. $this->assertTrue($context->isRequired('title'));
  647. $this->assertFalse($context->isRequired('body'));
  648. $this->assertNull($context->isRequired('Herp.derp.derp'));
  649. $this->assertNull($context->isRequired('nope'));
  650. $this->assertNull($context->isRequired(''));
  651. }
  652. /**
  653. * Test isRequired on associated entities.
  654. */
  655. public function testIsRequiredAssociatedHasMany(): void
  656. {
  657. $this->_setupTables();
  658. $comments = $this->getTableLocator()->get('Comments');
  659. $validator = $comments->getValidator();
  660. $validator->add('user_id', 'number', [
  661. 'rule' => 'numeric',
  662. ]);
  663. $row = new Article([
  664. 'title' => 'My title',
  665. 'comments' => [
  666. new Entity(['comment' => 'First comment']),
  667. new Entity(['comment' => 'Second comment']),
  668. ],
  669. ]);
  670. $context = new EntityContext([
  671. 'entity' => $row,
  672. 'table' => 'Articles',
  673. 'validator' => 'default',
  674. ]);
  675. $this->assertTrue($context->isRequired('comments.0.user_id'));
  676. $this->assertNull($context->isRequired('comments.0.other'));
  677. $this->assertNull($context->isRequired('user.0.other'));
  678. $this->assertNull($context->isRequired(''));
  679. }
  680. /**
  681. * Test isRequired on associated entities with boolean fields
  682. */
  683. public function testIsRequiredAssociatedHasManyBoolean(): void
  684. {
  685. $this->_setupTables();
  686. $comments = $this->getTableLocator()->get('Comments');
  687. $comments->getSchema()->addColumn('starred', 'boolean');
  688. $comments->getValidator()->add('starred', 'valid', ['rule' => 'boolean']);
  689. $row = new Article([
  690. 'title' => 'My title',
  691. 'comments' => [
  692. new Entity(['comment' => 'First comment']),
  693. ],
  694. ]);
  695. $context = new EntityContext([
  696. 'entity' => $row,
  697. 'table' => 'Articles',
  698. 'validator' => 'default',
  699. ]);
  700. $this->assertFalse($context->isRequired('comments.0.starred'));
  701. }
  702. /**
  703. * Test isRequired on associated entities with custom validators.
  704. *
  705. * Ensures that missing associations use the correct entity class
  706. * so provider methods work correctly.
  707. */
  708. public function testIsRequiredAssociatedCustomValidator(): void
  709. {
  710. $this->_setupTables();
  711. $users = $this->getTableLocator()->get('Users');
  712. $articles = $this->getTableLocator()->get('Articles');
  713. $validator = $articles->getValidator();
  714. $validator->notEmptyString('title', 'nope', function ($context) {
  715. return $context['providers']['entity']->isRequired();
  716. });
  717. $articles->setValidator('default', $validator);
  718. $row = new Entity([
  719. 'username' => 'mark',
  720. ]);
  721. $context = new EntityContext([
  722. 'entity' => $row,
  723. 'table' => 'Users',
  724. 'validator' => 'default',
  725. ]);
  726. $this->assertTrue($context->isRequired('articles.0.title'));
  727. }
  728. /**
  729. * Test isRequired on associated entities.
  730. */
  731. public function testIsRequiredAssociatedHasManyMissingObject(): void
  732. {
  733. $this->_setupTables();
  734. $comments = $this->getTableLocator()->get('Comments');
  735. $validator = $comments->getValidator();
  736. $validator->allowEmptyString('comment', null, function ($context) {
  737. return $context['providers']['entity']->isNew();
  738. });
  739. $row = new Article([
  740. 'title' => 'My title',
  741. 'comments' => [
  742. new Entity(['comment' => 'First comment'], ['markNew' => false]),
  743. ],
  744. ]);
  745. $context = new EntityContext([
  746. 'entity' => $row,
  747. 'table' => 'Articles',
  748. 'validator' => 'default',
  749. ]);
  750. $this->assertTrue(
  751. $context->isRequired('comments.0.comment'),
  752. 'comment is required as object is not new'
  753. );
  754. $this->assertFalse(
  755. $context->isRequired('comments.1.comment'),
  756. 'comment is not required as missing object is "new"'
  757. );
  758. }
  759. /**
  760. * Test isRequired on associated entities with custom validators.
  761. */
  762. public function testIsRequiredAssociatedValidator(): void
  763. {
  764. $this->_setupTables();
  765. $row = new Article([
  766. 'title' => 'My title',
  767. 'comments' => [
  768. new Entity(['comment' => 'First comment']),
  769. new Entity(['comment' => 'Second comment']),
  770. ],
  771. ]);
  772. $context = new EntityContext([
  773. 'entity' => $row,
  774. 'table' => 'Articles',
  775. 'validator' => [
  776. 'Articles' => 'create',
  777. 'Comments' => 'custom',
  778. ],
  779. ]);
  780. $this->assertTrue($context->isRequired('title'));
  781. $this->assertFalse($context->isRequired('body'));
  782. $this->assertTrue($context->isRequired('comments.0.comment'));
  783. $this->assertTrue($context->isRequired('comments.1.comment'));
  784. }
  785. /**
  786. * Test isRequired on associated entities.
  787. */
  788. public function testIsRequiredAssociatedBelongsTo(): void
  789. {
  790. $this->_setupTables();
  791. $row = new Article([
  792. 'title' => 'My title',
  793. 'user' => new Entity(['username' => 'Mark']),
  794. ]);
  795. $context = new EntityContext([
  796. 'entity' => $row,
  797. 'table' => 'Articles',
  798. 'validator' => [
  799. 'Articles' => 'create',
  800. 'Users' => 'custom',
  801. ],
  802. ]);
  803. $this->assertTrue($context->isRequired('user.username'));
  804. $this->assertNull($context->isRequired('user.first_name'));
  805. }
  806. /**
  807. * Test isRequired on associated join table entities.
  808. */
  809. public function testIsRequiredAssociatedJoinTable(): void
  810. {
  811. $this->_setupTables();
  812. $row = new Article([
  813. 'tags' => [
  814. new Tag([
  815. '_joinData' => new ArticlesTag([
  816. 'article_id' => 1,
  817. 'tag_id' => 2,
  818. ]),
  819. ]),
  820. ],
  821. ]);
  822. $context = new EntityContext([
  823. 'entity' => $row,
  824. 'table' => 'Articles',
  825. ]);
  826. $this->assertTrue($context->isRequired('tags.0._joinData.article_id'));
  827. $this->assertTrue($context->isRequired('tags.0._joinData.tag_id'));
  828. }
  829. /**
  830. * Test type() basic
  831. */
  832. public function testType(): void
  833. {
  834. $this->_setupTables();
  835. $row = new Article([
  836. 'title' => 'My title',
  837. 'body' => 'Some content',
  838. ]);
  839. $context = new EntityContext([
  840. 'entity' => $row,
  841. 'table' => 'Articles',
  842. ]);
  843. $this->assertSame('string', $context->type('title'));
  844. $this->assertSame('text', $context->type('body'));
  845. $this->assertSame('integer', $context->type('user_id'));
  846. $this->assertNull($context->type('nope'));
  847. }
  848. /**
  849. * Test getting types for associated records.
  850. */
  851. public function testTypeAssociated(): void
  852. {
  853. $this->_setupTables();
  854. $row = new Article([
  855. 'title' => 'My title',
  856. 'user' => new Entity(['username' => 'Mark']),
  857. ]);
  858. $context = new EntityContext([
  859. 'entity' => $row,
  860. 'table' => 'Articles',
  861. ]);
  862. $this->assertSame('string', $context->type('user.username'));
  863. $this->assertSame('text', $context->type('user.bio'));
  864. $this->assertNull($context->type('user.nope'));
  865. }
  866. /**
  867. * Test getting types for associated join data records.
  868. */
  869. public function testTypeAssociatedJoinData(): void
  870. {
  871. $this->_setupTables();
  872. $row = new Article([
  873. 'tags' => [
  874. new Tag([
  875. '_joinData' => new ArticlesTag([
  876. 'article_id' => 1,
  877. 'tag_id' => 2,
  878. ]),
  879. ]),
  880. ],
  881. ]);
  882. $context = new EntityContext([
  883. 'entity' => $row,
  884. 'table' => 'Articles',
  885. ]);
  886. $this->assertSame('integer', $context->type('tags.0._joinData.article_id'));
  887. $this->assertNull($context->type('tags.0._joinData.nonexistent'));
  888. // tests the fallback behavior
  889. $this->assertSame('integer', $context->type('tags.0._joinData._joinData.article_id'));
  890. $this->assertSame('integer', $context->type('tags.0._joinData.nonexistent.article_id'));
  891. $this->assertNull($context->type('tags.0._joinData._joinData.nonexistent'));
  892. $this->assertNull($context->type('tags.0._joinData.nonexistent'));
  893. }
  894. /**
  895. * Test attributes for fields.
  896. */
  897. public function testAttributes(): void
  898. {
  899. $this->_setupTables();
  900. $row = new Article([
  901. 'title' => 'My title',
  902. 'user' => new Entity(['username' => 'Mark']),
  903. 'tags' => [
  904. new Tag([
  905. '_joinData' => new ArticlesTag([
  906. 'article_id' => 1,
  907. 'tag_id' => 2,
  908. ]),
  909. ]),
  910. ],
  911. ]);
  912. $context = new EntityContext([
  913. 'entity' => $row,
  914. 'table' => 'Articles',
  915. ]);
  916. $expected = [
  917. 'length' => 255, 'precision' => null,
  918. 'null' => null, 'default' => null, 'comment' => null,
  919. ];
  920. $this->assertEquals($expected, $context->attributes('title'));
  921. $expected = [
  922. 'length' => null, 'precision' => null,
  923. 'null' => null, 'default' => null, 'comment' => null,
  924. ];
  925. $this->assertEquals($expected, $context->attributes('body'));
  926. $expected = [
  927. 'length' => 10, 'precision' => 3,
  928. 'null' => null, 'default' => null, 'comment' => null,
  929. ];
  930. $this->assertEquals($expected, $context->attributes('user.rating'));
  931. $expected = [
  932. 'length' => 11, 'precision' => null,
  933. 'null' => false, 'default' => null, 'comment' => null,
  934. ];
  935. $this->assertEquals($expected, $context->attributes('tags.0._joinData.article_id'));
  936. }
  937. /**
  938. * Test hasError
  939. */
  940. public function testHasError(): void
  941. {
  942. $this->_setupTables();
  943. $row = new Article([
  944. 'title' => 'My title',
  945. 'user' => new Entity(['username' => 'Mark']),
  946. ]);
  947. $row->setError('title', []);
  948. $row->setError('body', 'Gotta have one');
  949. $row->setError('user_id', ['Required field']);
  950. $context = new EntityContext([
  951. 'entity' => $row,
  952. 'table' => 'Articles',
  953. ]);
  954. $this->assertFalse($context->hasError('title'));
  955. $this->assertFalse($context->hasError('nope'));
  956. $this->assertTrue($context->hasError('body'));
  957. $this->assertTrue($context->hasError('user_id'));
  958. }
  959. /**
  960. * Test hasError on associated records
  961. */
  962. public function testHasErrorAssociated(): void
  963. {
  964. $this->_setupTables();
  965. $row = new Article([
  966. 'title' => 'My title',
  967. 'user' => new Entity(['username' => 'Mark']),
  968. ]);
  969. $row->setError('title', []);
  970. $row->setError('body', 'Gotta have one');
  971. $row->user->setError('username', ['Required']);
  972. $context = new EntityContext([
  973. 'entity' => $row,
  974. 'table' => 'Articles',
  975. ]);
  976. $this->assertTrue($context->hasError('user.username'));
  977. $this->assertFalse($context->hasError('user.nope'));
  978. $this->assertFalse($context->hasError('no.nope'));
  979. }
  980. /**
  981. * Test error
  982. */
  983. public function testError(): void
  984. {
  985. $this->_setupTables();
  986. $row = new Article([
  987. 'title' => 'My title',
  988. 'user' => new Entity(['username' => 'Mark']),
  989. ]);
  990. $row->setError('title', []);
  991. $row->setError('body', 'Gotta have one');
  992. $row->setError('user_id', ['Required field']);
  993. $row->user->setError('username', ['Required']);
  994. $context = new EntityContext([
  995. 'entity' => $row,
  996. 'table' => 'Articles',
  997. ]);
  998. $this->assertEquals([], $context->error('title'));
  999. $expected = ['Gotta have one'];
  1000. $this->assertEquals($expected, $context->error('body'));
  1001. $expected = ['Required'];
  1002. $this->assertEquals($expected, $context->error('user.username'));
  1003. }
  1004. /**
  1005. * Test error on associated entities.
  1006. */
  1007. public function testErrorAssociatedHasMany(): void
  1008. {
  1009. $this->_setupTables();
  1010. $comments = $this->getTableLocator()->get('Comments');
  1011. $row = new Article([
  1012. 'title' => 'My title',
  1013. 'comments' => [
  1014. new Entity(['comment' => '']),
  1015. new Entity(['comment' => 'Second comment']),
  1016. ],
  1017. ]);
  1018. $row->comments[0]->setError('comment', ['Is required']);
  1019. $row->comments[0]->setError('article_id', ['Is required']);
  1020. $context = new EntityContext([
  1021. 'entity' => $row,
  1022. 'table' => 'Articles',
  1023. 'validator' => 'default',
  1024. ]);
  1025. $this->assertEquals([], $context->error('title'));
  1026. $this->assertEquals([], $context->error('comments.0.user_id'));
  1027. $this->assertEquals([], $context->error('comments.0'));
  1028. $this->assertEquals(['Is required'], $context->error('comments.0.comment'));
  1029. $this->assertEquals(['Is required'], $context->error('comments.0.article_id'));
  1030. $this->assertEquals([], $context->error('comments.1'));
  1031. $this->assertEquals([], $context->error('comments.1.comment'));
  1032. $this->assertEquals([], $context->error('comments.1.article_id'));
  1033. }
  1034. /**
  1035. * Test error on associated join table entities.
  1036. */
  1037. public function testErrorAssociatedJoinTable(): void
  1038. {
  1039. $this->_setupTables();
  1040. $row = new Article([
  1041. 'tags' => [
  1042. new Tag([
  1043. '_joinData' => new ArticlesTag([
  1044. 'article_id' => 1,
  1045. ]),
  1046. ]),
  1047. ],
  1048. ]);
  1049. $row->tags[0]->_joinData->setError('tag_id', ['Is required']);
  1050. $context = new EntityContext([
  1051. 'entity' => $row,
  1052. 'table' => 'Articles',
  1053. ]);
  1054. $this->assertEquals([], $context->error('tags.0._joinData.article_id'));
  1055. $this->assertEquals(['Is required'], $context->error('tags.0._joinData.tag_id'));
  1056. }
  1057. /**
  1058. * Test error on nested validation
  1059. */
  1060. public function testErrorNestedValidator(): void
  1061. {
  1062. $this->_setupTables();
  1063. $row = new Article([
  1064. 'title' => 'My title',
  1065. 'options' => ['subpages' => ''],
  1066. ]);
  1067. $row->setError('options', ['subpages' => ['_empty' => 'required value']]);
  1068. $context = new EntityContext([
  1069. 'entity' => $row,
  1070. 'table' => 'Articles',
  1071. ]);
  1072. $expected = ['_empty' => 'required value'];
  1073. $this->assertEquals($expected, $context->error('options.subpages'));
  1074. }
  1075. /**
  1076. * Test error on nested validation
  1077. */
  1078. public function testErrorAssociatedNestedValidator(): void
  1079. {
  1080. $this->_setupTables();
  1081. $tagOne = new Tag(['name' => 'first-post']);
  1082. $tagTwo = new Tag(['name' => 'second-post']);
  1083. $tagOne->setError(
  1084. 'metadata',
  1085. ['description' => ['_empty' => 'required value']]
  1086. );
  1087. $row = new Article([
  1088. 'title' => 'My title',
  1089. 'tags' => [
  1090. $tagOne,
  1091. $tagTwo,
  1092. ],
  1093. ]);
  1094. $context = new EntityContext([
  1095. 'entity' => $row,
  1096. 'table' => 'Articles',
  1097. ]);
  1098. $expected = ['_empty' => 'required value'];
  1099. $this->assertSame([], $context->error('tags.0.notthere'));
  1100. $this->assertSame([], $context->error('tags.1.notthere'));
  1101. $this->assertEquals($expected, $context->error('tags.0.metadata.description'));
  1102. }
  1103. /**
  1104. * Setup tables for tests.
  1105. */
  1106. protected function _setupTables(): void
  1107. {
  1108. $articles = $this->getTableLocator()->get('Articles');
  1109. $articles->belongsTo('Users');
  1110. $articles->belongsToMany('Tags');
  1111. $articles->hasMany('Comments');
  1112. $articles->setEntityClass(Article::class);
  1113. $articlesTags = $this->getTableLocator()->get('ArticlesTags');
  1114. $comments = $this->getTableLocator()->get('Comments');
  1115. $users = $this->getTableLocator()->get('Users');
  1116. $users->hasMany('Articles');
  1117. $articles->setSchema([
  1118. 'id' => ['type' => 'integer', 'length' => 11, 'null' => false],
  1119. 'title' => ['type' => 'string', 'length' => 255],
  1120. 'user_id' => ['type' => 'integer', 'length' => 11, 'null' => false],
  1121. 'body' => ['type' => 'crazy_text', 'baseType' => 'text'],
  1122. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  1123. ]);
  1124. $articlesTags->setSchema([
  1125. 'article_id' => ['type' => 'integer', 'length' => 11, 'null' => false],
  1126. 'tag_id' => ['type' => 'integer', 'length' => 11, 'null' => false],
  1127. '_constraints' => ['unique_tag' => ['type' => 'primary', 'columns' => ['article_id', 'tag_id']]],
  1128. ]);
  1129. $users->setSchema([
  1130. 'id' => ['type' => 'integer', 'length' => 11],
  1131. 'username' => ['type' => 'string', 'length' => 255],
  1132. 'bio' => ['type' => 'text'],
  1133. 'rating' => ['type' => 'decimal', 'length' => 10, 'precision' => 3],
  1134. ]);
  1135. $validator = new Validator();
  1136. $validator->notEmptyString('title', 'Don\'t forget a title!');
  1137. $validator->add('title', 'minlength', [
  1138. 'rule' => ['minlength', 10],
  1139. ])
  1140. ->add('body', 'maxlength', [
  1141. 'rule' => ['maxlength', 1000],
  1142. ])->allowEmptyString('body');
  1143. $articles->setValidator('create', $validator);
  1144. $validator = new Validator();
  1145. $validator->add('username', 'length', [
  1146. 'rule' => ['minlength', 10],
  1147. ]);
  1148. $users->setValidator('custom', $validator);
  1149. $validator = new Validator();
  1150. $validator->add('comment', 'length', [
  1151. 'rule' => ['minlength', 10],
  1152. ]);
  1153. $comments->setValidator('custom', $validator);
  1154. $validator = new Validator();
  1155. $validator->requirePresence('article_id', 'create');
  1156. $validator->requirePresence('tag_id', 'create');
  1157. $articlesTags->setValidator('default', $validator);
  1158. }
  1159. /**
  1160. * Test the fieldnames method.
  1161. */
  1162. public function testFieldNames(): void
  1163. {
  1164. $context = new EntityContext([
  1165. 'entity' => new Entity(),
  1166. 'table' => 'Articles',
  1167. ]);
  1168. $articles = $this->getTableLocator()->get('Articles');
  1169. $this->assertEquals($articles->getSchema()->columns(), $context->fieldNames());
  1170. }
  1171. /**
  1172. * Test automatic entity provider setting
  1173. */
  1174. public function testValidatorEntityProvider(): void
  1175. {
  1176. $row = new Article([
  1177. 'title' => 'Test entity',
  1178. 'body' => 'Something new',
  1179. ]);
  1180. $context = new EntityContext([
  1181. 'entity' => $row,
  1182. 'table' => 'Articles',
  1183. ]);
  1184. $context->isRequired('title');
  1185. $articles = $this->getTableLocator()->get('Articles');
  1186. $this->assertSame($row, $articles->getValidator()->getProvider('entity'));
  1187. $row = new Article([
  1188. 'title' => 'First post',
  1189. 'user' => new Entity([
  1190. 'username' => 'mark',
  1191. 'fname' => 'Mark',
  1192. 'articles' => [
  1193. new Article(['title' => 'First post']),
  1194. new Article(['title' => 'Second post']),
  1195. ],
  1196. ]),
  1197. ]);
  1198. $context = new EntityContext([
  1199. 'entity' => $row,
  1200. 'table' => 'Articles',
  1201. ]);
  1202. $validator = $articles->getValidator();
  1203. $context->isRequired('user.articles.0.title');
  1204. $this->assertSame($row->user->articles[0], $validator->getProvider('entity'));
  1205. $context->isRequired('user.articles.1.title');
  1206. $this->assertSame($row->user->articles[1], $validator->getProvider('entity'));
  1207. $context->isRequired('title');
  1208. $this->assertSame($row, $validator->getProvider('entity'));
  1209. }
  1210. }