EntityContextTest.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386
  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\Core\Exception\CakeException;
  21. use Cake\ORM\Entity;
  22. use Cake\TestSuite\TestCase;
  23. use Cake\Validation\Validator;
  24. use Cake\View\Form\EntityContext;
  25. use PHPUnit\Framework\Attributes\DataProvider;
  26. use stdClass;
  27. use TestApp\Model\Entity\Article;
  28. use TestApp\Model\Entity\ArticlesTag;
  29. use TestApp\Model\Entity\Tag;
  30. /**
  31. * Entity context test case.
  32. */
  33. class EntityContextTest extends TestCase
  34. {
  35. /**
  36. * Fixtures to use.
  37. *
  38. * @var list<string>
  39. */
  40. protected array $fixtures = ['core.Articles', 'core.Comments', 'core.Tags', 'core.ArticlesTags'];
  41. /**
  42. * tests getRequiredMessage
  43. */
  44. public function testGetRequiredMessage(): void
  45. {
  46. $this->_setupTables();
  47. $context = new EntityContext([
  48. 'entity' => new Article(),
  49. 'table' => 'Articles',
  50. 'validator' => 'create',
  51. ]);
  52. $this->assertNull($context->getRequiredMessage('body'));
  53. $this->assertSame("Don't forget a title!", $context->getRequiredMessage('title'));
  54. }
  55. /**
  56. * Test getting entity back from context.
  57. */
  58. public function testEntity(): void
  59. {
  60. $row = new Article();
  61. $context = new EntityContext([
  62. 'entity' => $row,
  63. ]);
  64. $this->assertSame($row, $context->entity());
  65. }
  66. /**
  67. * Test getting primary key data.
  68. */
  69. public function testPrimaryKey(): void
  70. {
  71. $row = new Article();
  72. $context = new EntityContext([
  73. 'entity' => $row,
  74. ]);
  75. $this->assertEquals(['id'], $context->getPrimaryKey());
  76. }
  77. /**
  78. * Test isPrimaryKey
  79. */
  80. public function testIsPrimaryKey(): void
  81. {
  82. $this->_setupTables();
  83. $row = new Article();
  84. $context = new EntityContext([
  85. 'entity' => $row,
  86. ]);
  87. $this->assertTrue($context->isPrimaryKey('id'));
  88. $this->assertFalse($context->isPrimaryKey('title'));
  89. $this->assertTrue($context->isPrimaryKey('1.id'));
  90. $this->assertTrue($context->isPrimaryKey('Articles.1.id'));
  91. $this->assertTrue($context->isPrimaryKey('comments.0.id'));
  92. $this->assertTrue($context->isPrimaryKey('1.comments.0.id'));
  93. $this->assertFalse($context->isPrimaryKey('1.comments.0.comment'));
  94. $this->assertFalse($context->isPrimaryKey('Articles.1.comments.0.comment'));
  95. $this->assertTrue($context->isPrimaryKey('tags.0._joinData.article_id'));
  96. $this->assertTrue($context->isPrimaryKey('tags.0._joinData.tag_id'));
  97. }
  98. /**
  99. * Test isCreate on a single entity.
  100. */
  101. public function testIsCreateSingle(): void
  102. {
  103. $row = new Article();
  104. $context = new EntityContext([
  105. 'entity' => $row,
  106. ]);
  107. $this->assertTrue($context->isCreate());
  108. $row->setNew(false);
  109. $this->assertFalse($context->isCreate());
  110. $row->setNew(true);
  111. $this->assertTrue($context->isCreate());
  112. }
  113. /**
  114. * Test isCreate on a collection.
  115. *
  116. * @param mixed $collection
  117. */
  118. #[DataProvider('collectionProvider')]
  119. public function testIsCreateCollection($collection): void
  120. {
  121. $context = new EntityContext([
  122. 'entity' => $collection,
  123. ]);
  124. $this->assertTrue($context->isCreate());
  125. }
  126. /**
  127. * Test an invalid table scope throws an error.
  128. */
  129. public function testInvalidTable(): void
  130. {
  131. $this->expectException(CakeException::class);
  132. $this->expectExceptionMessage('Unable to find table class for current entity');
  133. $row = new stdClass();
  134. new EntityContext([
  135. 'entity' => $row,
  136. ]);
  137. }
  138. /**
  139. * Tests that passing a plain entity will give an error as it cannot be matched
  140. */
  141. public function testDefaultEntityError(): void
  142. {
  143. $this->expectException(CakeException::class);
  144. $this->expectExceptionMessage('Unable to find table class for current entity');
  145. new EntityContext([
  146. 'entity' => new Entity(),
  147. ]);
  148. }
  149. /**
  150. * Tests that the table can be derived from the entity source if it is present
  151. */
  152. public function testTableFromEntitySource(): void
  153. {
  154. $entity = new Entity();
  155. $entity->setSource('Articles');
  156. $context = new EntityContext([
  157. 'entity' => $entity,
  158. ]);
  159. $expected = ['id', 'author_id', 'title', 'body', 'published'];
  160. $this->assertEquals($expected, $context->fieldNames());
  161. }
  162. /**
  163. * Test operations with no entity.
  164. */
  165. public function testOperationsNoEntity(): void
  166. {
  167. $context = new EntityContext([
  168. 'table' => 'Articles',
  169. ]);
  170. $this->assertNull($context->val('title'));
  171. $this->assertNull($context->isRequired('title'));
  172. $this->assertFalse($context->hasError('title'));
  173. $this->assertSame('string', $context->type('title'));
  174. $this->assertEquals([], $context->error('title'));
  175. $attrs = $context->attributes('title');
  176. $this->assertArrayHasKey('length', $attrs);
  177. $this->assertArrayHasKey('precision', $attrs);
  178. }
  179. /**
  180. * Test operations that lack a table argument.
  181. */
  182. public function testOperationsNoTableArg(): void
  183. {
  184. $row = new Article([
  185. 'title' => 'Test entity',
  186. 'body' => 'Something new',
  187. ]);
  188. $row->setError('title', ['Title is required.']);
  189. $context = new EntityContext([
  190. 'entity' => $row,
  191. ]);
  192. $result = $context->val('title');
  193. $this->assertEquals($row->title, $result);
  194. $result = $context->error('title');
  195. $this->assertEquals($row->getError('title'), $result);
  196. $this->assertTrue($context->hasError('title'));
  197. }
  198. /**
  199. * Test collection operations that lack a table argument.
  200. *
  201. * @param mixed $collection
  202. */
  203. #[DataProvider('collectionProvider')]
  204. public function testCollectionOperationsNoTableArg($collection): void
  205. {
  206. $context = new EntityContext([
  207. 'entity' => $collection,
  208. ]);
  209. $result = $context->val('0.title');
  210. $this->assertSame('First post', $result);
  211. $result = $context->error('1.body');
  212. $this->assertEquals(['Not long enough'], $result);
  213. $this->assertNull($context->val('0'));
  214. }
  215. /**
  216. * Data provider for testing collections.
  217. *
  218. * @return array
  219. */
  220. public static function collectionProvider(): array
  221. {
  222. $one = new Article([
  223. 'title' => 'First post',
  224. 'body' => 'Stuff',
  225. 'user' => new Entity(['username' => 'mark']),
  226. ]);
  227. $one->setError('title', 'Required field');
  228. $two = new Article([
  229. 'title' => 'Second post',
  230. 'body' => 'Some text',
  231. 'user' => new Entity(['username' => 'jose']),
  232. ]);
  233. $two->setError('body', 'Not long enough');
  234. return [
  235. 'array' => [[$one, $two]],
  236. 'basic iterator' => [new ArrayObject([$one, $two])],
  237. 'array iterator' => [new ArrayIterator([$one, $two])],
  238. 'collection' => [new Collection([$one, $two])],
  239. ];
  240. }
  241. /**
  242. * Test operations on a collection of entities.
  243. *
  244. * @param mixed $collection
  245. */
  246. #[DataProvider('collectionProvider')]
  247. public function testValOnCollections($collection): void
  248. {
  249. $context = new EntityContext([
  250. 'entity' => $collection,
  251. 'table' => 'Articles',
  252. ]);
  253. $result = $context->val('0.title');
  254. $this->assertSame('First post', $result);
  255. $result = $context->val('0.user.username');
  256. $this->assertSame('mark', $result);
  257. $result = $context->val('1.title');
  258. $this->assertSame('Second post', $result);
  259. $result = $context->val('1.user.username');
  260. $this->assertSame('jose', $result);
  261. $this->assertNull($context->val('nope'));
  262. $this->assertNull($context->val('99.title'));
  263. }
  264. /**
  265. * Test operations on a collection of entities when prefixing with the
  266. * table name
  267. *
  268. * @param mixed $collection
  269. */
  270. #[DataProvider('collectionProvider')]
  271. public function testValOnCollectionsWithRootName($collection): void
  272. {
  273. $context = new EntityContext([
  274. 'entity' => $collection,
  275. 'table' => 'Articles',
  276. ]);
  277. $result = $context->val('Articles.0.title');
  278. $this->assertSame('First post', $result);
  279. $result = $context->val('Articles.0.user.username');
  280. $this->assertSame('mark', $result);
  281. $result = $context->val('Articles.1.title');
  282. $this->assertSame('Second post', $result);
  283. $result = $context->val('Articles.1.user.username');
  284. $this->assertSame('jose', $result);
  285. $this->assertNull($context->val('Articles.99.title'));
  286. }
  287. /**
  288. * Test error operations on a collection of entities.
  289. *
  290. * @param mixed $collection
  291. */
  292. #[DataProvider('collectionProvider')]
  293. public function testErrorsOnCollections($collection): void
  294. {
  295. $context = new EntityContext([
  296. 'entity' => $collection,
  297. 'table' => 'Articles',
  298. ]);
  299. $this->assertTrue($context->hasError('0.title'));
  300. $this->assertEquals(['Required field'], $context->error('0.title'));
  301. $this->assertFalse($context->hasError('0.body'));
  302. $this->assertFalse($context->hasError('1.title'));
  303. $this->assertEquals(['Not long enough'], $context->error('1.body'));
  304. $this->assertTrue($context->hasError('1.body'));
  305. $this->assertFalse($context->hasError('nope'));
  306. $this->assertFalse($context->hasError('99.title'));
  307. }
  308. /**
  309. * Test schema operations on a collection of entities.
  310. *
  311. * @param mixed $collection
  312. */
  313. #[DataProvider('collectionProvider')]
  314. public function testSchemaOnCollections($collection): void
  315. {
  316. $this->_setupTables();
  317. $context = new EntityContext([
  318. 'entity' => $collection,
  319. 'table' => 'Articles',
  320. ]);
  321. $this->assertSame('string', $context->type('0.title'));
  322. $this->assertSame('text', $context->type('1.body'));
  323. $this->assertSame('string', $context->type('0.user.username'));
  324. $this->assertSame('string', $context->type('1.user.username'));
  325. $this->assertSame('string', $context->type('99.title'));
  326. $this->assertNull($context->type('0.nope'));
  327. $expected = [
  328. 'length' => 255, 'precision' => null,
  329. 'null' => null, 'default' => null, 'comment' => null,
  330. ];
  331. $this->assertEquals($expected, $context->attributes('0.user.username'));
  332. }
  333. /**
  334. * Test validation operations on a collection of entities.
  335. *
  336. * @param mixed $collection
  337. */
  338. #[DataProvider('collectionProvider')]
  339. public function testValidatorsOnCollections($collection): void
  340. {
  341. $this->_setupTables();
  342. $context = new EntityContext([
  343. 'entity' => $collection,
  344. 'table' => 'Articles',
  345. 'validator' => [
  346. 'Articles' => 'create',
  347. 'Users' => 'custom',
  348. ],
  349. ]);
  350. $this->assertNull($context->isRequired('nope'));
  351. $this->assertTrue($context->isRequired('0.title'));
  352. $this->assertTrue($context->isRequired('0.user.username'));
  353. $this->assertFalse($context->isRequired('1.body'));
  354. $this->assertTrue($context->isRequired('99.title'));
  355. $this->assertNull($context->isRequired('99.nope'));
  356. }
  357. /**
  358. * Test reading data.
  359. */
  360. public function testValBasic(): void
  361. {
  362. $row = new Article([
  363. 'title' => 'Test entity',
  364. 'body' => 'Something new',
  365. ]);
  366. $context = new EntityContext([
  367. 'entity' => $row,
  368. 'table' => 'Articles',
  369. ]);
  370. $result = $context->val('title');
  371. $this->assertEquals($row->title, $result);
  372. $result = $context->val('body');
  373. $this->assertEquals($row->body, $result);
  374. $result = $context->val('nope');
  375. $this->assertNull($result);
  376. }
  377. /**
  378. * Test reading invalid data.
  379. */
  380. public function testValInvalid(): void
  381. {
  382. $row = new Article([
  383. 'title' => 'Valid title',
  384. ]);
  385. $row->setInvalidField('title', 'Invalid title');
  386. $context = new EntityContext([
  387. 'entity' => $row,
  388. 'table' => 'Articles',
  389. ]);
  390. $result = $context->val('title');
  391. $this->assertSame('Invalid title', $result);
  392. }
  393. /**
  394. * Test default values when entity is an array.
  395. */
  396. public function testValDefaultArray(): void
  397. {
  398. $context = new EntityContext([
  399. 'entity' => new Article([
  400. 'prop' => ['title' => 'foo'],
  401. ]),
  402. 'table' => 'Articles',
  403. ]);
  404. $this->assertSame('foo', $context->val('prop.title', ['default' => 'bar']));
  405. $this->assertSame('bar', $context->val('prop.nope', ['default' => 'bar']));
  406. }
  407. /**
  408. * Test reading array values from an entity.
  409. */
  410. public function testValGetArrayValue(): void
  411. {
  412. $row = new Article([
  413. 'title' => 'Test entity',
  414. 'types' => [1, 2, 3],
  415. 'tag' => [
  416. 'name' => 'Test tag',
  417. ],
  418. 'author' => new Entity([
  419. 'roles' => ['admin', 'publisher'],
  420. 'aliases' => new ArrayObject(['dave', 'david']),
  421. ]),
  422. ]);
  423. $context = new EntityContext([
  424. 'entity' => $row,
  425. 'table' => 'Articles',
  426. ]);
  427. $result = $context->val('types');
  428. $this->assertEquals($row->types, $result);
  429. $result = $context->val('author.roles');
  430. $this->assertEquals($row->author->roles, $result);
  431. $result = $context->val('tag.name');
  432. $this->assertEquals($row->tag['name'], $result);
  433. $result = $context->val('author.aliases.0');
  434. $this->assertEquals($row->author->aliases[0], $result, 'ArrayAccess can be read');
  435. $this->assertNull($context->val('author.aliases.3'));
  436. $this->assertNull($context->val('tag.nope'));
  437. $this->assertNull($context->val('author.roles.3'));
  438. }
  439. /**
  440. * Test reading values from associated entities.
  441. */
  442. public function testValAssociated(): void
  443. {
  444. $row = new Article([
  445. 'title' => 'Test entity',
  446. 'user' => new Entity([
  447. 'username' => 'mark',
  448. 'fname' => 'Mark',
  449. ]),
  450. 'comments' => [
  451. new Entity(['comment' => 'Test comment']),
  452. new Entity(['comment' => 'Second comment']),
  453. ],
  454. ]);
  455. $context = new EntityContext([
  456. 'entity' => $row,
  457. 'table' => 'Articles',
  458. ]);
  459. $result = $context->val('user.fname');
  460. $this->assertEquals($row->user->fname, $result);
  461. $result = $context->val('comments.0.comment');
  462. $this->assertEquals($row->comments[0]->comment, $result);
  463. $result = $context->val('comments.1.comment');
  464. $this->assertEquals($row->comments[1]->comment, $result);
  465. $result = $context->val('comments.0.nope');
  466. $this->assertNull($result);
  467. $result = $context->val('comments.0.nope.no_way');
  468. $this->assertNull($result);
  469. }
  470. /**
  471. * Tests that trying to get values from missing associations returns null
  472. */
  473. public function testValMissingAssociation(): void
  474. {
  475. $row = new Article([
  476. 'id' => 1,
  477. ]);
  478. $context = new EntityContext([
  479. 'entity' => $row,
  480. 'table' => 'Articles',
  481. ]);
  482. $result = $context->val('id');
  483. $this->assertEquals($row->id, $result);
  484. $this->assertNull($context->val('profile.id'));
  485. }
  486. /**
  487. * Test reading values from associated entities.
  488. */
  489. public function testValAssociatedHasMany(): void
  490. {
  491. $row = new Article([
  492. 'title' => 'First post',
  493. 'user' => new Entity([
  494. 'username' => 'mark',
  495. 'fname' => 'Mark',
  496. 'articles' => [
  497. new Article(['title' => 'First post']),
  498. new Article(['title' => 'Second post']),
  499. ],
  500. ]),
  501. ]);
  502. $context = new EntityContext([
  503. 'entity' => $row,
  504. 'table' => 'Articles',
  505. ]);
  506. $result = $context->val('user.articles.0.title');
  507. $this->assertSame('First post', $result);
  508. $result = $context->val('user.articles.1.title');
  509. $this->assertSame('Second post', $result);
  510. }
  511. /**
  512. * Test reading values for magic _ids input
  513. */
  514. public function testValAssociatedDefaultIds(): void
  515. {
  516. $row = new Article([
  517. 'title' => 'First post',
  518. 'user' => new Entity([
  519. 'username' => 'mark',
  520. 'fname' => 'Mark',
  521. 'sections' => [
  522. new Entity(['title' => 'PHP', 'id' => 1]),
  523. new Entity(['title' => 'Javascript', 'id' => 2]),
  524. ],
  525. ]),
  526. ]);
  527. $context = new EntityContext([
  528. 'entity' => $row,
  529. 'table' => 'Articles',
  530. ]);
  531. $result = $context->val('user.sections._ids');
  532. $this->assertEquals([1, 2], $result);
  533. }
  534. /**
  535. * Test reading values for magic _ids input
  536. */
  537. public function testValAssociatedCustomIds(): void
  538. {
  539. $this->_setupTables();
  540. $row = new Article([
  541. 'title' => 'First post',
  542. 'user' => new Entity([
  543. 'username' => 'mark',
  544. 'fname' => 'Mark',
  545. 'sections' => [
  546. new Entity(['title' => 'PHP', 'thing' => 1]),
  547. new Entity(['title' => 'Javascript', 'thing' => 4]),
  548. ],
  549. ]),
  550. ]);
  551. $context = new EntityContext([
  552. 'entity' => $row,
  553. 'table' => 'Articles',
  554. ]);
  555. $this->getTableLocator()->get('Users')->belongsToMany('Sections');
  556. $this->getTableLocator()->get('Sections')->setPrimaryKey('thing');
  557. $result = $context->val('user.sections._ids');
  558. $this->assertEquals([1, 4], $result);
  559. }
  560. /**
  561. * Test getting default value from table schema.
  562. */
  563. public function testValSchemaDefault(): void
  564. {
  565. $table = $this->getTableLocator()->get('Articles');
  566. $column = $table->getSchema()->getColumn('title');
  567. $table->getSchema()->addColumn('title', ['default' => 'default title'] + $column);
  568. $row = $table->newEmptyEntity();
  569. $context = new EntityContext([
  570. 'entity' => $row,
  571. 'table' => 'Articles',
  572. ]);
  573. $result = $context->val('title');
  574. $this->assertSame('default title', $result);
  575. }
  576. /**
  577. * Test getting association default value from table schema.
  578. */
  579. public function testValAssociatedSchemaDefault(): void
  580. {
  581. $table = $this->getTableLocator()->get('Articles');
  582. $associatedTable = $table->hasMany('Comments')->getTarget();
  583. $column = $associatedTable->getSchema()->getColumn('comment');
  584. $associatedTable->getSchema()->addColumn('comment', ['default' => 'default comment'] + $column);
  585. $row = $table->newEmptyEntity();
  586. $context = new EntityContext([
  587. 'entity' => $row,
  588. 'table' => 'Articles',
  589. ]);
  590. $result = $context->val('comments.0.comment');
  591. $this->assertSame('default comment', $result);
  592. }
  593. /**
  594. * Test getting association join table default value from table schema.
  595. */
  596. public function testValAssociatedJoinTableSchemaDefault(): void
  597. {
  598. $table = $this->getTableLocator()->get('Articles');
  599. $joinTable = $table
  600. ->belongsToMany('Tags')
  601. ->setThrough('ArticlesTags')
  602. ->junction();
  603. $joinTable->getSchema()->addColumn('column', [
  604. 'default' => 'default join table column value',
  605. 'type' => 'text',
  606. ]);
  607. $row = $table->newEmptyEntity();
  608. $context = new EntityContext([
  609. 'entity' => $row,
  610. 'table' => 'Articles',
  611. ]);
  612. $result = $context->val('tags.0._joinData.column');
  613. $this->assertSame('default join table column value', $result);
  614. }
  615. /**
  616. * Test validator for boolean fields.
  617. */
  618. public function testIsRequiredBooleanField(): void
  619. {
  620. $this->_setupTables();
  621. $context = new EntityContext([
  622. 'entity' => new Entity(),
  623. 'table' => 'Articles',
  624. ]);
  625. $articles = $this->getTableLocator()->get('Articles');
  626. $articles->getSchema()->addColumn('comments_on', [
  627. 'type' => 'boolean',
  628. ]);
  629. $validator = $articles->getValidator();
  630. $validator->add('comments_on', 'is_bool', [
  631. 'rule' => 'boolean',
  632. ]);
  633. $articles->setValidator('default', $validator);
  634. $this->assertNull($context->isRequired('title'));
  635. }
  636. /**
  637. * Test validator as a string.
  638. */
  639. public function testIsRequiredStringValidator(): void
  640. {
  641. $this->_setupTables();
  642. $context = new EntityContext([
  643. 'entity' => new Entity(),
  644. 'table' => 'Articles',
  645. 'validator' => 'create',
  646. ]);
  647. $this->assertTrue($context->isRequired('title'));
  648. $this->assertFalse($context->isRequired('body'));
  649. $this->assertNull($context->isRequired('Herp.derp.derp'));
  650. $this->assertNull($context->isRequired('nope'));
  651. $this->assertNull($context->isRequired(''));
  652. }
  653. /**
  654. * Test isRequired on associated entities.
  655. */
  656. public function testIsRequiredAssociatedHasMany(): void
  657. {
  658. $this->_setupTables();
  659. $comments = $this->getTableLocator()->get('Comments');
  660. $validator = $comments->getValidator();
  661. $validator->add('user_id', 'number', [
  662. 'rule' => 'numeric',
  663. ]);
  664. $row = new Article([
  665. 'title' => 'My title',
  666. 'comments' => [
  667. new Entity(['comment' => 'First comment']),
  668. new Entity(['comment' => 'Second comment']),
  669. ],
  670. ]);
  671. $context = new EntityContext([
  672. 'entity' => $row,
  673. 'table' => 'Articles',
  674. 'validator' => 'default',
  675. ]);
  676. $this->assertTrue($context->isRequired('comments.0.user_id'));
  677. $this->assertNull($context->isRequired('comments.0.other'));
  678. $this->assertNull($context->isRequired('user.0.other'));
  679. $this->assertNull($context->isRequired(''));
  680. }
  681. /**
  682. * Test isRequired on associated entities with boolean fields
  683. */
  684. public function testIsRequiredAssociatedHasManyBoolean(): void
  685. {
  686. $this->_setupTables();
  687. $comments = $this->getTableLocator()->get('Comments');
  688. $comments->getSchema()->addColumn('starred', 'boolean');
  689. $comments->getValidator()->add('starred', 'valid', ['rule' => 'boolean']);
  690. $row = new Article([
  691. 'title' => 'My title',
  692. 'comments' => [
  693. new Entity(['comment' => 'First comment']),
  694. ],
  695. ]);
  696. $context = new EntityContext([
  697. 'entity' => $row,
  698. 'table' => 'Articles',
  699. 'validator' => 'default',
  700. ]);
  701. $this->assertFalse($context->isRequired('comments.0.starred'));
  702. }
  703. /**
  704. * Test isRequired on associated entities with custom validators.
  705. *
  706. * Ensures that missing associations use the correct entity class
  707. * so provider methods work correctly.
  708. */
  709. public function testIsRequiredAssociatedCustomValidator(): void
  710. {
  711. $this->_setupTables();
  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. $row = new Article([
  1011. 'title' => 'My title',
  1012. 'comments' => [
  1013. new Entity(['comment' => '']),
  1014. new Entity(['comment' => 'Second comment']),
  1015. ],
  1016. ]);
  1017. $row->comments[0]->setError('comment', ['Is required']);
  1018. $row->comments[0]->setError('article_id', ['Is required']);
  1019. $context = new EntityContext([
  1020. 'entity' => $row,
  1021. 'table' => 'Articles',
  1022. 'validator' => 'default',
  1023. ]);
  1024. $this->assertEquals([], $context->error('title'));
  1025. $this->assertEquals([], $context->error('comments.0.user_id'));
  1026. $this->assertEquals([], $context->error('comments.0'));
  1027. $this->assertEquals(['Is required'], $context->error('comments.0.comment'));
  1028. $this->assertEquals(['Is required'], $context->error('comments.0.article_id'));
  1029. $this->assertEquals([], $context->error('comments.1'));
  1030. $this->assertEquals([], $context->error('comments.1.comment'));
  1031. $this->assertEquals([], $context->error('comments.1.article_id'));
  1032. }
  1033. /**
  1034. * Test error on associated join table entities.
  1035. */
  1036. public function testErrorAssociatedJoinTable(): void
  1037. {
  1038. $this->_setupTables();
  1039. $row = new Article([
  1040. 'tags' => [
  1041. new Tag([
  1042. '_joinData' => new ArticlesTag([
  1043. 'article_id' => 1,
  1044. ]),
  1045. ]),
  1046. ],
  1047. ]);
  1048. $row->tags[0]->_joinData->setError('tag_id', ['Is required']);
  1049. $context = new EntityContext([
  1050. 'entity' => $row,
  1051. 'table' => 'Articles',
  1052. ]);
  1053. $this->assertEquals([], $context->error('tags.0._joinData.article_id'));
  1054. $this->assertEquals(['Is required'], $context->error('tags.0._joinData.tag_id'));
  1055. }
  1056. /**
  1057. * Test error on nested validation
  1058. */
  1059. public function testErrorNestedValidator(): void
  1060. {
  1061. $this->_setupTables();
  1062. $row = new Article([
  1063. 'title' => 'My title',
  1064. 'options' => ['subpages' => ''],
  1065. ]);
  1066. $row->setError('options', ['subpages' => ['_empty' => 'required value']]);
  1067. $context = new EntityContext([
  1068. 'entity' => $row,
  1069. 'table' => 'Articles',
  1070. ]);
  1071. $expected = ['_empty' => 'required value'];
  1072. $this->assertEquals($expected, $context->error('options.subpages'));
  1073. }
  1074. /**
  1075. * Test error on nested validation
  1076. */
  1077. public function testErrorAssociatedNestedValidator(): void
  1078. {
  1079. $this->_setupTables();
  1080. $tagOne = new Tag(['name' => 'first-post']);
  1081. $tagTwo = new Tag(['name' => 'second-post']);
  1082. $tagOne->setError(
  1083. 'metadata',
  1084. ['description' => ['_empty' => 'required value']]
  1085. );
  1086. $row = new Article([
  1087. 'title' => 'My title',
  1088. 'tags' => [
  1089. $tagOne,
  1090. $tagTwo,
  1091. ],
  1092. ]);
  1093. $context = new EntityContext([
  1094. 'entity' => $row,
  1095. 'table' => 'Articles',
  1096. ]);
  1097. $expected = ['_empty' => 'required value'];
  1098. $this->assertSame([], $context->error('tags.0.notthere'));
  1099. $this->assertSame([], $context->error('tags.1.notthere'));
  1100. $this->assertEquals($expected, $context->error('tags.0.metadata.description'));
  1101. }
  1102. /**
  1103. * Setup tables for tests.
  1104. */
  1105. protected function _setupTables(): void
  1106. {
  1107. $articles = $this->getTableLocator()->get('Articles');
  1108. $articles->belongsTo('Users');
  1109. $articles->belongsToMany('Tags');
  1110. $articles->hasMany('Comments');
  1111. $articles->setEntityClass(Article::class);
  1112. $articlesTags = $this->getTableLocator()->get('ArticlesTags');
  1113. $comments = $this->getTableLocator()->get('Comments');
  1114. $users = $this->getTableLocator()->get('Users');
  1115. $users->hasMany('Articles');
  1116. $articles->setSchema([
  1117. 'id' => ['type' => 'integer', 'length' => 11, 'null' => false],
  1118. 'title' => ['type' => 'string', 'length' => 255],
  1119. 'user_id' => ['type' => 'integer', 'length' => 11, 'null' => false],
  1120. 'body' => ['type' => 'crazy_text', 'baseType' => 'text'],
  1121. '_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]],
  1122. ]);
  1123. $articlesTags->setSchema([
  1124. 'article_id' => ['type' => 'integer', 'length' => 11, 'null' => false],
  1125. 'tag_id' => ['type' => 'integer', 'length' => 11, 'null' => false],
  1126. '_constraints' => ['unique_tag' => ['type' => 'primary', 'columns' => ['article_id', 'tag_id']]],
  1127. ]);
  1128. $users->setSchema([
  1129. 'id' => ['type' => 'integer', 'length' => 11],
  1130. 'username' => ['type' => 'string', 'length' => 255],
  1131. 'bio' => ['type' => 'text'],
  1132. 'rating' => ['type' => 'decimal', 'length' => 10, 'precision' => 3],
  1133. ]);
  1134. $validator = new Validator();
  1135. $validator->notEmptyString('title', "Don't forget a title!");
  1136. $validator->add('title', 'minlength', [
  1137. 'rule' => ['minlength', 10],
  1138. ])
  1139. ->add('body', 'maxlength', [
  1140. 'rule' => ['maxlength', 1000],
  1141. ])->allowEmptyString('body');
  1142. $articles->setValidator('create', $validator);
  1143. $validator = new Validator();
  1144. $validator->add('username', 'length', [
  1145. 'rule' => ['minlength', 10],
  1146. ]);
  1147. $users->setValidator('custom', $validator);
  1148. $validator = new Validator();
  1149. $validator->add('comment', 'length', [
  1150. 'rule' => ['minlength', 10],
  1151. ]);
  1152. $comments->setValidator('custom', $validator);
  1153. $validator = new Validator();
  1154. $validator->requirePresence('article_id', 'create');
  1155. $validator->requirePresence('tag_id', 'create');
  1156. $articlesTags->setValidator('default', $validator);
  1157. }
  1158. /**
  1159. * Test the fieldnames method.
  1160. */
  1161. public function testFieldNames(): void
  1162. {
  1163. $context = new EntityContext([
  1164. 'entity' => new Entity(),
  1165. 'table' => 'Articles',
  1166. ]);
  1167. $articles = $this->getTableLocator()->get('Articles');
  1168. $this->assertEquals($articles->getSchema()->columns(), $context->fieldNames());
  1169. }
  1170. /**
  1171. * Test automatic entity provider setting
  1172. */
  1173. public function testValidatorEntityProvider(): void
  1174. {
  1175. $row = new Article([
  1176. 'title' => 'Test entity',
  1177. 'body' => 'Something new',
  1178. ]);
  1179. $context = new EntityContext([
  1180. 'entity' => $row,
  1181. 'table' => 'Articles',
  1182. ]);
  1183. $context->isRequired('title');
  1184. $articles = $this->getTableLocator()->get('Articles');
  1185. $this->assertSame($row, $articles->getValidator()->getProvider('entity'));
  1186. $row = new Article([
  1187. 'title' => 'First post',
  1188. 'user' => new Entity([
  1189. 'username' => 'mark',
  1190. 'fname' => 'Mark',
  1191. 'articles' => [
  1192. new Article(['title' => 'First post']),
  1193. new Article(['title' => 'Second post']),
  1194. ],
  1195. ]),
  1196. ]);
  1197. $context = new EntityContext([
  1198. 'entity' => $row,
  1199. 'table' => 'Articles',
  1200. ]);
  1201. $validator = $articles->getValidator();
  1202. $context->isRequired('user.articles.0.title');
  1203. $this->assertSame($row->user->articles[0], $validator->getProvider('entity'));
  1204. $context->isRequired('user.articles.1.title');
  1205. $this->assertSame($row->user->articles[1], $validator->getProvider('entity'));
  1206. $context->isRequired('title');
  1207. $this->assertSame($row, $validator->getProvider('entity'));
  1208. }
  1209. }