RulesCheckerIntegrationTest.php 59 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849
  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\ORM;
  17. use ArrayObject;
  18. use Cake\Database\Driver\Sqlserver;
  19. use Cake\Database\Exception\DatabaseException;
  20. use Cake\Datasource\ConnectionManager;
  21. use Cake\Datasource\EntityInterface;
  22. use Cake\Event\EventInterface;
  23. use Cake\I18n\I18n;
  24. use Cake\ORM\Entity;
  25. use Cake\ORM\RulesChecker;
  26. use Cake\ORM\Table;
  27. use Cake\TestSuite\TestCase;
  28. use Closure;
  29. use stdClass;
  30. /**
  31. * Tests the integration between the ORM and the domain checker
  32. */
  33. class RulesCheckerIntegrationTest extends TestCase
  34. {
  35. /**
  36. * Fixtures to be loaded
  37. *
  38. * @var array<string>
  39. */
  40. protected array $fixtures = [
  41. 'core.Articles', 'core.Tags', 'core.ArticlesTags', 'core.Authors', 'core.Comments',
  42. 'core.SpecialTags', 'core.Categories', 'core.SiteArticles', 'core.SiteAuthors',
  43. 'core.UniqueAuthors',
  44. ];
  45. /**
  46. * Tests saving belongsTo association and get a validation error
  47. *
  48. * @group save
  49. */
  50. public function testSaveBelongsToWithValidationError(): void
  51. {
  52. $entity = new Entity([
  53. 'title' => 'A Title',
  54. 'body' => 'A body',
  55. ]);
  56. $entity->author = new Entity([
  57. 'name' => 'Jose',
  58. ]);
  59. $table = $this->getTableLocator()->get('articles');
  60. $table->belongsTo('authors');
  61. $table->getAssociation('authors')
  62. ->getTarget()
  63. ->rulesChecker()
  64. ->add(
  65. function (Entity $author, array $options) use ($table) {
  66. $this->assertSame($options['repository'], $table->getAssociation('authors')->getTarget());
  67. return false;
  68. },
  69. ['errorField' => 'name', 'message' => 'This is an error']
  70. );
  71. $this->assertFalse($table->save($entity));
  72. $this->assertTrue($entity->isNew());
  73. $this->assertTrue($entity->author->isNew());
  74. $this->assertNull($entity->get('author_id'));
  75. $this->assertNotEmpty($entity->author->getError('name'));
  76. $this->assertEquals(['This is an error'], $entity->author->getError('name'));
  77. }
  78. /**
  79. * Tests saving hasOne association and returning a validation error will
  80. * abort the saving process
  81. *
  82. * @group save
  83. */
  84. public function testSaveHasOneWithValidationError(): void
  85. {
  86. $entity = new Entity([
  87. 'name' => 'Jose',
  88. ]);
  89. $entity->article = new Entity([
  90. 'title' => 'A Title',
  91. 'body' => 'A body',
  92. ]);
  93. $table = $this->getTableLocator()->get('authors');
  94. $table->hasOne('articles');
  95. $table->getAssociation('articles')
  96. ->getTarget()
  97. ->rulesChecker()
  98. ->add(
  99. function (EntityInterface $entity) {
  100. return false;
  101. },
  102. ['errorField' => 'title', 'message' => 'This is an error']
  103. );
  104. $this->assertFalse($table->save($entity));
  105. $this->assertTrue($entity->isNew());
  106. $this->assertTrue($entity->article->isNew());
  107. $this->assertNull($entity->article->id);
  108. $this->assertNull($entity->article->get('author_id'));
  109. $this->assertFalse($entity->article->isDirty('author_id'));
  110. $this->assertNotEmpty($entity->article->getError('title'));
  111. $this->assertSame('A Title', $entity->article->getInvalidField('title'));
  112. }
  113. /**
  114. * Tests saving multiple entities in a hasMany association and getting and
  115. * error while saving one of them. It should abort all the save operation
  116. * when options are set to defaults
  117. */
  118. public function testSaveHasManyWithErrorsAtomic(): void
  119. {
  120. $entity = new Entity([
  121. 'name' => 'Jose',
  122. ]);
  123. $entity->articles = [
  124. new Entity([
  125. 'title' => '1',
  126. 'body' => 'A body',
  127. ]),
  128. new Entity([
  129. 'title' => 'Another Title',
  130. 'body' => 'Another body',
  131. ]),
  132. ];
  133. $table = $this->getTableLocator()->get('authors');
  134. $table->hasMany('articles');
  135. $table->getAssociation('articles')
  136. ->getTarget()
  137. ->rulesChecker()
  138. ->add(
  139. function (Entity $entity, $options) use ($table) {
  140. $this->assertSame($table, $options['_sourceTable']);
  141. return $entity->title === '1';
  142. },
  143. ['errorField' => 'title', 'message' => 'This is an error']
  144. );
  145. $this->assertFalse($table->save($entity));
  146. $this->assertTrue($entity->isNew());
  147. $this->assertTrue($entity->articles[0]->isNew());
  148. $this->assertTrue($entity->articles[1]->isNew());
  149. $this->assertNull($entity->articles[0]->id);
  150. $this->assertNull($entity->articles[1]->id);
  151. $this->assertNull($entity->articles[0]->author_id);
  152. $this->assertNull($entity->articles[1]->author_id);
  153. $this->assertEmpty($entity->articles[0]->getErrors());
  154. $this->assertNotEmpty($entity->articles[1]->getErrors());
  155. }
  156. /**
  157. * Tests that it is possible to continue saving hasMany associations
  158. * even if any of the records fail validation when atomic is set
  159. * to false
  160. */
  161. public function testSaveHasManyWithErrorsNonAtomic(): void
  162. {
  163. $entity = new Entity([
  164. 'name' => 'Jose',
  165. ]);
  166. $entity->articles = [
  167. new Entity([
  168. 'title' => 'A title',
  169. 'body' => 'A body',
  170. ]),
  171. new Entity([
  172. 'title' => '1',
  173. 'body' => 'Another body',
  174. ]),
  175. ];
  176. $table = $this->getTableLocator()->get('authors');
  177. $table->hasMany('articles');
  178. $table->getAssociation('articles')
  179. ->getTarget()
  180. ->rulesChecker()
  181. ->add(
  182. function (Entity $article) {
  183. return is_numeric($article->title);
  184. },
  185. ['errorField' => 'title', 'message' => 'This is an error']
  186. );
  187. $result = $table->save($entity, ['atomic' => false]);
  188. $this->assertSame($entity, $result);
  189. $this->assertFalse($entity->isNew());
  190. $this->assertTrue($entity->articles[0]->isNew());
  191. $this->assertFalse($entity->articles[1]->isNew());
  192. $this->assertSame(4, $entity->articles[1]->id);
  193. $this->assertNull($entity->articles[0]->id);
  194. $this->assertNotEmpty($entity->articles[0]->getError('title'));
  195. }
  196. /**
  197. * Tests saving belongsToMany records with a validation error in a joint entity
  198. *
  199. * @group save
  200. */
  201. public function testSaveBelongsToManyWithValidationErrorInJointEntity(): void
  202. {
  203. $entity = new Entity([
  204. 'title' => 'A Title',
  205. 'body' => 'A body',
  206. ]);
  207. $entity->tags = [
  208. new Entity([
  209. 'name' => 'Something New',
  210. ]),
  211. new Entity([
  212. 'name' => '100',
  213. ]),
  214. ];
  215. $table = $this->getTableLocator()->get('articles');
  216. $table->belongsToMany('tags');
  217. $table->getAssociation('tags')
  218. ->junction()
  219. ->rulesChecker()
  220. ->add(function (Entity $entity) {
  221. return $entity->article_id > 4;
  222. });
  223. $this->assertFalse($table->save($entity));
  224. $this->assertTrue($entity->isNew());
  225. $this->assertTrue($entity->tags[0]->isNew());
  226. $this->assertTrue($entity->tags[1]->isNew());
  227. $this->assertNull($entity->tags[0]->id);
  228. $this->assertNull($entity->tags[1]->id);
  229. $this->assertNull($entity->tags[0]->_joinData);
  230. $this->assertNull($entity->tags[1]->_joinData);
  231. }
  232. /**
  233. * Tests saving belongsToMany records with a validation error in a joint entity
  234. * and atomic set to false
  235. *
  236. * @group save
  237. */
  238. public function testSaveBelongsToManyWithValidationErrorInJointEntityNonAtomic(): void
  239. {
  240. $entity = new Entity([
  241. 'title' => 'A Title',
  242. 'body' => 'A body',
  243. ]);
  244. $entity->tags = [
  245. new Entity([
  246. 'name' => 'Something New',
  247. ]),
  248. new Entity([
  249. 'name' => 'New one',
  250. ]),
  251. ];
  252. $table = $this->getTableLocator()->get('articles');
  253. $table->belongsToMany('tags');
  254. $table->getAssociation('tags')
  255. ->junction()
  256. ->rulesChecker()
  257. ->add(function (Entity $entity) {
  258. return $entity->tag_id > 4;
  259. });
  260. $this->assertSame($entity, $table->save($entity, ['atomic' => false]));
  261. $this->assertFalse($entity->isNew());
  262. $this->assertFalse($entity->tags[0]->isNew());
  263. $this->assertFalse($entity->tags[1]->isNew());
  264. $this->assertSame(4, $entity->tags[0]->id);
  265. $this->assertSame(5, $entity->tags[1]->id);
  266. $this->assertTrue($entity->tags[0]->_joinData->isNew());
  267. $this->assertSame(4, $entity->tags[1]->_joinData->article_id);
  268. $this->assertSame(5, $entity->tags[1]->_joinData->tag_id);
  269. }
  270. /**
  271. * Test adding rule with name
  272. *
  273. * @group save
  274. */
  275. public function testAddingRuleWithName(): void
  276. {
  277. $entity = new Entity([
  278. 'name' => 'larry',
  279. ]);
  280. $table = $this->getTableLocator()->get('Authors');
  281. $rules = $table->rulesChecker();
  282. $rules->add(
  283. function () {
  284. return false;
  285. },
  286. 'ruleName',
  287. ['errorField' => 'name']
  288. );
  289. $this->assertFalse($table->save($entity));
  290. $this->assertEquals(['ruleName' => 'invalid'], $entity->getError('name'));
  291. }
  292. /**
  293. * Ensure that add(isUnique()) only invokes a rule once.
  294. */
  295. public function testIsUniqueRuleSingleInvocation(): void
  296. {
  297. $entity = new Entity([
  298. 'name' => 'larry',
  299. ]);
  300. $table = $this->getTableLocator()->get('Authors');
  301. $rules = $table->rulesChecker();
  302. $rules->add($rules->isUnique(['name']), '_isUnique', ['errorField' => 'title']);
  303. $this->assertFalse($table->save($entity));
  304. $this->assertEquals(
  305. ['_isUnique' => 'This value is already in use'],
  306. $entity->getError('title'),
  307. 'Provided field should have errors'
  308. );
  309. $this->assertEmpty($entity->getError('name'), 'Errors should not apply to original field.');
  310. }
  311. /**
  312. * Tests the isUnique domain rule
  313. *
  314. * @group save
  315. */
  316. public function testIsUniqueDomainRule(): void
  317. {
  318. $entity = new Entity([
  319. 'name' => 'larry',
  320. ]);
  321. $table = $this->getTableLocator()->get('Authors');
  322. $rules = $table->rulesChecker();
  323. $rules->add($rules->isUnique(['name']));
  324. $this->assertFalse($table->save($entity));
  325. $this->assertEquals(['_isUnique' => 'This value is already in use'], $entity->getError('name'));
  326. $entity->name = 'jose';
  327. $this->assertSame($entity, $table->save($entity));
  328. $entity = $table->get(1);
  329. $entity->setDirty('name', true);
  330. $this->assertSame($entity, $table->save($entity));
  331. }
  332. /**
  333. * Tests isUnique with multiple fields
  334. *
  335. * @group save
  336. */
  337. public function testIsUniqueMultipleFields(): void
  338. {
  339. $entity = new Entity([
  340. 'author_id' => 1,
  341. 'title' => 'First Article',
  342. ]);
  343. $table = $this->getTableLocator()->get('Articles');
  344. $rules = $table->rulesChecker();
  345. $rules->add($rules->isUnique(['title', 'author_id'], 'Nope'));
  346. $this->assertFalse($table->save($entity));
  347. $this->assertEquals(['title' => ['_isUnique' => 'Nope']], $entity->getErrors());
  348. $entity->clean();
  349. $entity->author_id = 2;
  350. $this->assertSame($entity, $table->save($entity));
  351. }
  352. /**
  353. * Tests isUnique with non-unique null values
  354. */
  355. public function testIsUniqueNonUniqueNulls(): void
  356. {
  357. $table = $this->getTableLocator()->get('UniqueAuthors');
  358. $rules = $table->rulesChecker();
  359. $rules->add($rules->isUnique(
  360. ['first_author_id', 'second_author_id'],
  361. ['allowMultipleNulls' => false]
  362. ));
  363. $entity = new Entity([
  364. 'first_author_id' => null,
  365. 'second_author_id' => 1,
  366. ]);
  367. $this->assertFalse($table->save($entity));
  368. $this->assertEquals(['first_author_id' => ['_isUnique' => 'This value is already in use']], $entity->getErrors());
  369. }
  370. /**
  371. * Tests isUnique with allowMultipleNulls
  372. *
  373. * @group save
  374. */
  375. public function testIsUniqueAllowMultipleNulls(): void
  376. {
  377. $this->skipIf(ConnectionManager::get('test')->getDriver() instanceof Sqlserver);
  378. $table = $this->getTableLocator()->get('UniqueAuthors');
  379. $rules = $table->rulesChecker();
  380. $rules->add($rules->isUnique(
  381. ['first_author_id', 'second_author_id']
  382. ));
  383. $entity = new Entity([
  384. 'first_author_id' => null,
  385. 'second_author_id' => 1,
  386. ]);
  387. $this->assertNotEmpty($table->save($entity));
  388. $entity->first_author_id = 2;
  389. $this->assertSame($entity, $table->save($entity));
  390. $entity = new Entity([
  391. 'first_author_id' => 2,
  392. 'second_author_id' => 1,
  393. ]);
  394. $this->assertFalse($table->save($entity));
  395. $this->assertEquals(['first_author_id' => ['_isUnique' => 'This value is already in use']], $entity->getErrors());
  396. }
  397. /**
  398. * Tests the existsIn domain rule
  399. *
  400. * @group save
  401. */
  402. public function testExistsInDomainRule(): void
  403. {
  404. $entity = new Entity([
  405. 'title' => 'An Article',
  406. 'author_id' => 500,
  407. ]);
  408. $table = $this->getTableLocator()->get('Articles');
  409. $table->belongsTo('Authors');
  410. $rules = $table->rulesChecker();
  411. $rules->add($rules->existsIn('author_id', 'Authors'));
  412. $this->assertFalse($table->save($entity));
  413. $this->assertEquals(['_existsIn' => 'This value does not exist'], $entity->getError('author_id'));
  414. }
  415. /**
  416. * Ensure that add(existsIn()) only invokes a rule once.
  417. */
  418. public function testExistsInRuleSingleInvocation(): void
  419. {
  420. $entity = new Entity([
  421. 'title' => 'larry',
  422. 'author_id' => 500,
  423. ]);
  424. $table = $this->getTableLocator()->get('Articles');
  425. $table->belongsTo('Authors');
  426. $rules = $table->rulesChecker();
  427. $rules->add($rules->existsIn('author_id', 'Authors'), '_existsIn', ['errorField' => 'other']);
  428. $this->assertFalse($table->save($entity));
  429. $this->assertEquals(
  430. ['_existsIn' => 'This value does not exist'],
  431. $entity->getError('other'),
  432. 'Provided field should have errors'
  433. );
  434. $this->assertEmpty($entity->getError('author_id'), 'Errors should not apply to original field.');
  435. }
  436. /**
  437. * Tests the existsIn domain rule when passing an object
  438. *
  439. * @group save
  440. */
  441. public function testExistsInDomainRuleWithObject(): void
  442. {
  443. $entity = new Entity([
  444. 'title' => 'An Article',
  445. 'author_id' => 500,
  446. ]);
  447. $table = $this->getTableLocator()->get('Articles');
  448. $rules = $table->rulesChecker();
  449. $rules->add($rules->existsIn('author_id', $this->getTableLocator()->get('Authors'), 'Nope'));
  450. $this->assertFalse($table->save($entity));
  451. $this->assertEquals(['_existsIn' => 'Nope'], $entity->getError('author_id'));
  452. }
  453. /**
  454. * ExistsIn uses the schema to verify that nullable fields are ok.
  455. */
  456. public function testExistsInNullValue(): void
  457. {
  458. $entity = new Entity([
  459. 'title' => 'An Article',
  460. 'author_id' => null,
  461. ]);
  462. $table = $this->getTableLocator()->get('Articles');
  463. $table->belongsTo('Authors');
  464. $rules = $table->rulesChecker();
  465. $rules->add($rules->existsIn('author_id', 'Authors'));
  466. $this->assertEquals($entity, $table->save($entity));
  467. $this->assertEquals([], $entity->getError('author_id'));
  468. }
  469. /**
  470. * Test ExistsIn on a new entity that doesn't have the field populated.
  471. *
  472. * This use case is important for saving records and their
  473. * associated belongsTo records in one pass.
  474. */
  475. public function testExistsInNotNullValueNewEntity(): void
  476. {
  477. $entity = new Entity([
  478. 'name' => 'A Category',
  479. ]);
  480. $table = $this->getTableLocator()->get('Categories');
  481. $table->belongsTo('Categories', [
  482. 'foreignKey' => 'parent_id',
  483. 'bindingKey' => 'id',
  484. ]);
  485. $rules = $table->rulesChecker();
  486. $rules->add($rules->existsIn('parent_id', 'Categories'));
  487. $this->assertTrue($table->checkRules($entity, RulesChecker::CREATE));
  488. $this->assertEmpty($entity->getError('parent_id'));
  489. }
  490. /**
  491. * Tests exists in uses the bindingKey of the association
  492. */
  493. public function testExistsInWithBindingKey(): void
  494. {
  495. $entity = new Entity([
  496. 'title' => 'An Article',
  497. ]);
  498. $table = $this->getTableLocator()->get('Articles');
  499. $table->belongsTo('Authors', [
  500. 'bindingKey' => 'name',
  501. 'foreignKey' => 'title',
  502. ]);
  503. $rules = $table->rulesChecker();
  504. $rules->add($rules->existsIn('title', 'Authors'));
  505. $this->assertFalse($table->save($entity));
  506. $this->assertNotEmpty($entity->getError('title'));
  507. $entity->clean();
  508. $entity->title = 'larry';
  509. $this->assertEquals($entity, $table->save($entity));
  510. }
  511. /**
  512. * Tests existsIn with invalid associations
  513. *
  514. * @group save
  515. */
  516. public function testExistsInInvalidAssociation(): void
  517. {
  518. $this->expectException(DatabaseException::class);
  519. $this->expectExceptionMessage('ExistsIn rule for `author_id` is invalid. `NotValid` is not associated with `Cake\ORM\Table`.');
  520. $entity = new Entity([
  521. 'title' => 'An Article',
  522. 'author_id' => 500,
  523. ]);
  524. $table = $this->getTableLocator()->get('Articles');
  525. $table->belongsTo('Authors');
  526. $rules = $table->rulesChecker();
  527. $rules->add($rules->existsIn('author_id', 'NotValid'));
  528. $table->save($entity);
  529. }
  530. /**
  531. * Tests existsIn does not prevent new entities from saving if parent entity is new
  532. */
  533. public function testExistsInHasManyNewEntities(): void
  534. {
  535. $table = $this->getTableLocator()->get('Articles');
  536. $table->hasMany('Comments');
  537. $table->Comments->belongsTo('Articles');
  538. $rules = $table->Comments->rulesChecker();
  539. $rules->add($rules->existsIn(['article_id'], $table));
  540. $article = $table->newEntity([
  541. 'title' => 'new article',
  542. 'comments' => [
  543. $table->Comments->newEntity([
  544. 'user_id' => 1,
  545. 'comment' => 'comment 1',
  546. ]),
  547. $table->Comments->newEntity([
  548. 'user_id' => 1,
  549. 'comment' => 'comment 2',
  550. ]),
  551. ],
  552. ]);
  553. $this->assertNotFalse($table->save($article));
  554. }
  555. /**
  556. * Tests existsIn does not prevent new entities from saving if parent entity is new,
  557. * getting the parent entity from the association
  558. */
  559. public function testExistsInHasManyNewEntitiesViaAssociation(): void
  560. {
  561. $table = $this->getTableLocator()->get('Articles');
  562. $table->hasMany('Comments');
  563. $table->Comments->belongsTo('Articles');
  564. $rules = $table->Comments->rulesChecker();
  565. $rules->add($rules->existsIn(['article_id'], 'Articles'));
  566. $article = $table->newEntity([
  567. 'title' => 'test',
  568. ]);
  569. $article->comments = [
  570. $table->Comments->newEntity([
  571. 'user_id' => 1,
  572. 'comment' => 'test',
  573. ]),
  574. ];
  575. $this->assertNotFalse($table->save($article));
  576. }
  577. /**
  578. * Tests the checkRules save option
  579. *
  580. * @group save
  581. */
  582. public function testSkipRulesChecking(): void
  583. {
  584. $entity = new Entity([
  585. 'title' => 'An Article',
  586. 'author_id' => 500,
  587. ]);
  588. $table = $this->getTableLocator()->get('Articles');
  589. $rules = $table->rulesChecker();
  590. $rules->add($rules->existsIn('author_id', $this->getTableLocator()->get('Authors'), 'Nope'));
  591. $this->assertSame($entity, $table->save($entity, ['checkRules' => false]));
  592. }
  593. /**
  594. * Tests the beforeRules event
  595. *
  596. * @group save
  597. */
  598. public function testUseBeforeRules(): void
  599. {
  600. $entity = new Entity([
  601. 'title' => 'An Article',
  602. 'author_id' => 500,
  603. ]);
  604. $table = $this->getTableLocator()->get('Articles');
  605. $rules = $table->rulesChecker();
  606. $rules->add($rules->existsIn('author_id', $this->getTableLocator()->get('Authors'), 'Nope'));
  607. $table->getEventManager()->on(
  608. 'Model.beforeRules',
  609. function (EventInterface $event, EntityInterface $entity, ArrayObject $options, $operation) {
  610. $this->assertEquals(
  611. [
  612. 'atomic' => true,
  613. 'associated' => true,
  614. 'checkRules' => true,
  615. 'checkExisting' => true,
  616. '_primary' => true,
  617. '_cleanOnSuccess' => true,
  618. ],
  619. $options->getArrayCopy()
  620. );
  621. $this->assertSame('create', $operation);
  622. $event->stopPropagation();
  623. return true;
  624. }
  625. );
  626. $this->assertSame($entity, $table->save($entity));
  627. }
  628. /**
  629. * Tests the afterRules event
  630. *
  631. * @group save
  632. */
  633. public function testUseAfterRules(): void
  634. {
  635. $entity = new Entity([
  636. 'title' => 'An Article',
  637. 'author_id' => 500,
  638. ]);
  639. $table = $this->getTableLocator()->get('Articles');
  640. $rules = $table->rulesChecker();
  641. $rules->add($rules->existsIn('author_id', $this->getTableLocator()->get('Authors'), 'Nope'));
  642. $table->getEventManager()->on(
  643. 'Model.afterRules',
  644. function (EventInterface $event, EntityInterface $entity, ArrayObject $options, $result, $operation) {
  645. $this->assertEquals(
  646. [
  647. 'atomic' => true,
  648. 'associated' => true,
  649. 'checkRules' => true,
  650. 'checkExisting' => true,
  651. '_primary' => true,
  652. '_cleanOnSuccess' => true,
  653. ],
  654. $options->getArrayCopy()
  655. );
  656. $this->assertSame('create', $operation);
  657. $this->assertFalse($result);
  658. $event->stopPropagation();
  659. return true;
  660. }
  661. );
  662. $this->assertSame($entity, $table->save($entity));
  663. }
  664. /**
  665. * Tests that rules can be changed using the buildRules event
  666. *
  667. * @group save
  668. */
  669. public function testUseBuildRulesEvent(): void
  670. {
  671. $entity = new Entity([
  672. 'title' => 'An Article',
  673. 'author_id' => 500,
  674. ]);
  675. $table = $this->getTableLocator()->get('Articles');
  676. $table->getEventManager()->on('Model.buildRules', function (EventInterface $event, RulesChecker $rules): void {
  677. $rules->add($rules->existsIn('author_id', $this->getTableLocator()->get('Authors'), 'Nope'));
  678. });
  679. $this->assertFalse($table->save($entity));
  680. }
  681. /**
  682. * Tests isUnique with untouched fields
  683. *
  684. * @group save
  685. */
  686. public function testIsUniqueWithCleanFields(): void
  687. {
  688. $table = $this->getTableLocator()->get('Articles');
  689. $entity = $table->get(1);
  690. $rules = $table->rulesChecker();
  691. $rules->add($rules->isUnique(['title', 'author_id'], 'Nope'));
  692. $entity->body = 'Foo';
  693. $this->assertSame($entity, $table->save($entity));
  694. $entity->title = 'Third Article';
  695. $this->assertFalse($table->save($entity));
  696. }
  697. /**
  698. * Tests isUnique rule with conflicting columns
  699. *
  700. * @group save
  701. */
  702. public function testIsUniqueAliasPrefix(): void
  703. {
  704. $entity = new Entity([
  705. 'title' => 'An Article',
  706. 'author_id' => 1,
  707. ]);
  708. $table = $this->getTableLocator()->get('Articles');
  709. $table->belongsTo('Authors');
  710. $rules = $table->rulesChecker();
  711. $rules->add($rules->isUnique(['author_id']));
  712. $table->Authors->getEventManager()->on('Model.beforeFind', function (EventInterface $event, $query): void {
  713. $query->leftJoin(['a2' => 'authors']);
  714. });
  715. $this->assertFalse($table->save($entity));
  716. $this->assertEquals(['_isUnique' => 'This value is already in use'], $entity->getError('author_id'));
  717. }
  718. /**
  719. * Tests the existsIn rule when passing non dirty fields
  720. *
  721. * @group save
  722. */
  723. public function testExistsInWithCleanFields(): void
  724. {
  725. $table = $this->getTableLocator()->get('Articles');
  726. $table->belongsTo('Authors');
  727. $rules = $table->rulesChecker();
  728. $rules->add($rules->existsIn('author_id', 'Authors'));
  729. $entity = $table->get(1);
  730. $entity->title = 'Foo';
  731. $entity->author_id = 1000;
  732. $entity->setDirty('author_id', false);
  733. $this->assertSame($entity, $table->save($entity));
  734. }
  735. /**
  736. * Tests the existsIn with conflicting columns
  737. *
  738. * @group save
  739. */
  740. public function testExistsInAliasPrefix(): void
  741. {
  742. $entity = new Entity([
  743. 'title' => 'An Article',
  744. 'author_id' => 500,
  745. ]);
  746. $table = $this->getTableLocator()->get('Articles');
  747. $table->belongsTo('Authors');
  748. $rules = $table->rulesChecker();
  749. $rules->add($rules->existsIn('author_id', 'Authors'));
  750. $table->Authors->getEventManager()->on('Model.beforeFind', function (EventInterface $event, $query): void {
  751. $query->leftJoin(['a2' => 'authors']);
  752. });
  753. $this->assertFalse($table->save($entity));
  754. $this->assertEquals(['_existsIn' => 'This value does not exist'], $entity->getError('author_id'));
  755. }
  756. /**
  757. * Tests that using an array in existsIn() sets the error message correctly
  758. */
  759. public function testExistsInErrorWithArrayField(): void
  760. {
  761. $entity = new Entity([
  762. 'title' => 'An Article',
  763. 'author_id' => 500,
  764. ]);
  765. $table = $this->getTableLocator()->get('Articles');
  766. $table->belongsTo('Authors');
  767. $rules = $table->rulesChecker();
  768. $rules->add($rules->existsIn(['author_id'], 'Authors'));
  769. $this->assertFalse($table->save($entity));
  770. $this->assertEquals(['_existsIn' => 'This value does not exist'], $entity->getError('author_id'));
  771. }
  772. /**
  773. * Tests new allowNullableNulls flag with author id set to null
  774. */
  775. public function testExistsInAllowNullableNullsOn(): void
  776. {
  777. $entity = new Entity([
  778. 'id' => 10,
  779. 'author_id' => null,
  780. 'site_id' => 1,
  781. 'name' => 'New Site Article without Author',
  782. ]);
  783. $table = $this->getTableLocator()->get('SiteArticles');
  784. $table->belongsTo('SiteAuthors');
  785. $rules = $table->rulesChecker();
  786. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
  787. 'allowNullableNulls' => true,
  788. ]));
  789. $this->assertInstanceOf('Cake\ORM\Entity', $table->save($entity));
  790. }
  791. /**
  792. * Tests new allowNullableNulls flag with author id set to null
  793. */
  794. public function testExistsInAllowNullableNullsOff(): void
  795. {
  796. $entity = new Entity([
  797. 'id' => 10,
  798. 'author_id' => null,
  799. 'site_id' => 1,
  800. 'name' => 'New Site Article without Author',
  801. ]);
  802. $table = $this->getTableLocator()->get('SiteArticles');
  803. $table->belongsTo('SiteAuthors');
  804. $rules = $table->rulesChecker();
  805. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
  806. 'allowNullableNulls' => false,
  807. ]));
  808. $this->assertFalse($table->save($entity));
  809. }
  810. /**
  811. * Tests new allowNullableNulls flag with author id set to null
  812. */
  813. public function testExistsInAllowNullableNullsDefaultValue(): void
  814. {
  815. $entity = new Entity([
  816. 'id' => 10,
  817. 'author_id' => null,
  818. 'site_id' => 1,
  819. 'name' => 'New Site Article without Author',
  820. ]);
  821. $table = $this->getTableLocator()->get('SiteArticles');
  822. $table->belongsTo('SiteAuthors');
  823. $rules = $table->rulesChecker();
  824. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors'));
  825. $this->assertFalse($table->save($entity));
  826. }
  827. /**
  828. * Tests new allowNullableNulls flag with author id set to null
  829. */
  830. public function testExistsInAllowNullableNullsCustomMessage(): void
  831. {
  832. $entity = new Entity([
  833. 'id' => 10,
  834. 'author_id' => null,
  835. 'site_id' => 1,
  836. 'name' => 'New Site Article without Author',
  837. ]);
  838. $table = $this->getTableLocator()->get('SiteArticles');
  839. $table->belongsTo('SiteAuthors');
  840. $rules = $table->rulesChecker();
  841. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
  842. 'allowNullableNulls' => false,
  843. 'message' => 'Niente',
  844. ]));
  845. $this->assertFalse($table->save($entity));
  846. $this->assertEquals(['author_id' => ['_existsIn' => 'Niente']], $entity->getErrors());
  847. }
  848. /**
  849. * Tests new allowNullableNulls flag with author id set to 1
  850. */
  851. public function testExistsInAllowNullableNullsOnAllKeysSet(): void
  852. {
  853. $entity = new Entity([
  854. 'id' => 10,
  855. 'author_id' => 1,
  856. 'site_id' => 1,
  857. 'name' => 'New Site Article with Author',
  858. ]);
  859. $table = $this->getTableLocator()->get('SiteArticles');
  860. $table->belongsTo('SiteAuthors');
  861. $rules = $table->rulesChecker();
  862. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', ['allowNullableNulls' => true]));
  863. $this->assertInstanceOf('Cake\ORM\Entity', $table->save($entity));
  864. }
  865. /**
  866. * Tests new allowNullableNulls flag with author id set to 1
  867. */
  868. public function testExistsInAllowNullableNullsOffAllKeysSet(): void
  869. {
  870. $entity = new Entity([
  871. 'id' => 10,
  872. 'author_id' => 1,
  873. 'site_id' => 1,
  874. 'name' => 'New Site Article with Author',
  875. ]);
  876. $table = $this->getTableLocator()->get('SiteArticles');
  877. $table->belongsTo('SiteAuthors');
  878. $rules = $table->rulesChecker();
  879. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', ['allowNullableNulls' => false]));
  880. $this->assertInstanceOf('Cake\ORM\Entity', $table->save($entity));
  881. }
  882. /**
  883. * Tests new allowNullableNulls flag with author id set to 1
  884. */
  885. public function testExistsInAllowNullableNullsOnAllKeysCustomMessage(): void
  886. {
  887. $entity = new Entity([
  888. 'id' => 10,
  889. 'author_id' => 1,
  890. 'site_id' => 1,
  891. 'name' => 'New Site Article with Author',
  892. ]);
  893. $table = $this->getTableLocator()->get('SiteArticles');
  894. $table->belongsTo('SiteAuthors');
  895. $rules = $table->rulesChecker();
  896. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
  897. 'allowNullableNulls' => true,
  898. 'message' => 'will not error']));
  899. $this->assertInstanceOf('Cake\ORM\Entity', $table->save($entity));
  900. }
  901. /**
  902. * Tests new allowNullableNulls flag with author id set to 99999999 (does not exist)
  903. */
  904. public function testExistsInAllowNullableNullsOnInvalidKey(): void
  905. {
  906. $entity = new Entity([
  907. 'id' => 10,
  908. 'author_id' => 99999999,
  909. 'site_id' => 1,
  910. 'name' => 'New Site Article with Author',
  911. ]);
  912. $table = $this->getTableLocator()->get('SiteArticles');
  913. $table->belongsTo('SiteAuthors');
  914. $rules = $table->rulesChecker();
  915. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
  916. 'allowNullableNulls' => true,
  917. 'message' => 'will error']));
  918. $this->assertFalse($table->save($entity));
  919. $this->assertEquals(['author_id' => ['_existsIn' => 'will error']], $entity->getErrors());
  920. }
  921. /**
  922. * Tests new allowNullableNulls flag with author id set to 99999999 (does not exist)
  923. * and site_id set to 99999999 (does not exist)
  924. */
  925. public function testExistsInAllowNullableNullsOnInvalidKeys(): void
  926. {
  927. $entity = new Entity([
  928. 'id' => 10,
  929. 'author_id' => 99999999,
  930. 'site_id' => 99999999,
  931. 'name' => 'New Site Article with Author',
  932. ]);
  933. $table = $this->getTableLocator()->get('SiteArticles');
  934. $table->belongsTo('SiteAuthors');
  935. $rules = $table->rulesChecker();
  936. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
  937. 'allowNullableNulls' => true,
  938. 'message' => 'will error']));
  939. $this->assertFalse($table->save($entity));
  940. $this->assertEquals(['author_id' => ['_existsIn' => 'will error']], $entity->getErrors());
  941. }
  942. /**
  943. * Tests new allowNullableNulls flag with author id set to 1 (does exist)
  944. * and site_id set to 99999999 (does not exist)
  945. */
  946. public function testExistsInAllowNullableNullsOnInvalidKeySecond(): void
  947. {
  948. $entity = new Entity([
  949. 'id' => 10,
  950. 'author_id' => 1,
  951. 'site_id' => 99999999,
  952. 'name' => 'New Site Article with Author',
  953. ]);
  954. $table = $this->getTableLocator()->get('SiteArticles');
  955. $table->belongsTo('SiteAuthors');
  956. $rules = $table->rulesChecker();
  957. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
  958. 'allowNullableNulls' => true,
  959. 'message' => 'will error']));
  960. $this->assertFalse($table->save($entity));
  961. $this->assertEquals(['author_id' => ['_existsIn' => 'will error']], $entity->getErrors());
  962. }
  963. /**
  964. * Tests new allowNullableNulls with saveMany
  965. */
  966. public function testExistsInAllowNullableNullsSaveMany(): void
  967. {
  968. $entities = [
  969. new Entity([
  970. 'id' => 1,
  971. 'author_id' => null,
  972. 'site_id' => 1,
  973. 'name' => 'New Site Article without Author',
  974. ]),
  975. new Entity([
  976. 'id' => 2,
  977. 'author_id' => 1,
  978. 'site_id' => 1,
  979. 'name' => 'New Site Article with Author',
  980. ]),
  981. ];
  982. $table = $this->getTableLocator()->get('SiteArticles');
  983. $table->belongsTo('SiteAuthors');
  984. $rules = $table->rulesChecker();
  985. $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
  986. 'allowNullableNulls' => true,
  987. 'message' => 'will error with array_combine warning']));
  988. $result = $table->saveMany($entities);
  989. $this->assertCount(2, $result);
  990. $this->assertInstanceOf(Entity::class, $result[0]);
  991. $this->assertEmpty($result[0]->getErrors());
  992. $this->assertInstanceOf(Entity::class, $result[1]);
  993. $this->assertEmpty($result[1]->getErrors());
  994. }
  995. /**
  996. * Tests using rules to prevent delete operations
  997. *
  998. * @group delete
  999. */
  1000. public function testDeleteRules(): void
  1001. {
  1002. $table = $this->getTableLocator()->get('Articles');
  1003. $rules = $table->rulesChecker();
  1004. $rules->addDelete(function ($entity) {
  1005. return false;
  1006. });
  1007. $entity = $table->get(1);
  1008. $this->assertFalse($table->delete($entity));
  1009. }
  1010. /**
  1011. * Checks that it is possible to pass custom options to rules when saving
  1012. *
  1013. * @group save
  1014. */
  1015. public function testCustomOptionsPassingSave(): void
  1016. {
  1017. $entity = new Entity([
  1018. 'name' => 'jose',
  1019. ]);
  1020. $table = $this->getTableLocator()->get('Authors');
  1021. $rules = $table->rulesChecker();
  1022. $rules->add(function ($entity, $options) {
  1023. $this->assertSame('bar', $options['foo']);
  1024. $this->assertSame('option', $options['another']);
  1025. return false;
  1026. }, ['another' => 'option']);
  1027. $this->assertFalse($table->save($entity, ['foo' => 'bar']));
  1028. }
  1029. /**
  1030. * Tests passing custom options to rules from delete
  1031. *
  1032. * @group delete
  1033. */
  1034. public function testCustomOptionsPassingDelete(): void
  1035. {
  1036. $table = $this->getTableLocator()->get('Articles');
  1037. $rules = $table->rulesChecker();
  1038. $rules->addDelete(function ($entity, $options) {
  1039. $this->assertSame('bar', $options['foo']);
  1040. $this->assertSame('option', $options['another']);
  1041. return false;
  1042. }, ['another' => 'option']);
  1043. $entity = $table->get(1);
  1044. $this->assertFalse($table->delete($entity, ['foo' => 'bar']));
  1045. }
  1046. /**
  1047. * Test adding rules that return error string
  1048. *
  1049. * @group save
  1050. */
  1051. public function testCustomErrorMessageFromRule(): void
  1052. {
  1053. $entity = new Entity([
  1054. 'name' => 'larry',
  1055. ]);
  1056. $table = $this->getTableLocator()->get('Authors');
  1057. $rules = $table->rulesChecker();
  1058. $rules->add(function () {
  1059. return 'So much nope';
  1060. }, ['errorField' => 'name']);
  1061. $this->assertFalse($table->save($entity));
  1062. $this->assertEquals(['So much nope'], $entity->getError('name'));
  1063. }
  1064. /**
  1065. * Test adding rules with no errorField do not accept strings
  1066. *
  1067. * @group save
  1068. */
  1069. public function testCustomErrorMessageFromRuleNoErrorField(): void
  1070. {
  1071. $entity = new Entity([
  1072. 'name' => 'larry',
  1073. ]);
  1074. $table = $this->getTableLocator()->get('Authors');
  1075. $rules = $table->rulesChecker();
  1076. $rules->add(function () {
  1077. return 'So much nope';
  1078. });
  1079. $this->assertFalse($table->save($entity));
  1080. $this->assertEmpty($entity->getErrors());
  1081. }
  1082. /**
  1083. * Tests that using existsIn for a hasMany association will not be called
  1084. * as the foreign key for the association was automatically validated already.
  1085. *
  1086. * @group save
  1087. */
  1088. public function testAvoidExistsInOnAutomaticSaving(): void
  1089. {
  1090. $entity = new Entity([
  1091. 'name' => 'Jose',
  1092. ]);
  1093. $entity->articles = [
  1094. new Entity([
  1095. 'title' => '1',
  1096. 'body' => 'A body',
  1097. ]),
  1098. new Entity([
  1099. 'title' => 'Another Title',
  1100. 'body' => 'Another body',
  1101. ]),
  1102. ];
  1103. $table = $this->getTableLocator()->get('authors');
  1104. $table->hasMany('articles');
  1105. $table->getAssociation('articles')->belongsTo('authors');
  1106. $checker = $table->getAssociation('articles')->getTarget()->rulesChecker();
  1107. $checker->add(function ($entity, $options) use ($checker) {
  1108. $rule = $checker->existsIn('author_id', 'authors');
  1109. $id = $entity->author_id;
  1110. $entity->author_id = 5000;
  1111. $result = $rule($entity, $options);
  1112. $this->assertTrue($result);
  1113. $entity->author_id = $id;
  1114. return true;
  1115. });
  1116. $this->assertSame($entity, $table->save($entity));
  1117. }
  1118. /**
  1119. * Tests the existsIn domain rule respects the conditions set for the associations
  1120. *
  1121. * @group save
  1122. */
  1123. public function testExistsInDomainRuleWithAssociationConditions(): void
  1124. {
  1125. $entity = new Entity([
  1126. 'title' => 'An Article',
  1127. 'author_id' => 1,
  1128. ]);
  1129. $table = $this->getTableLocator()->get('Articles');
  1130. $table->belongsTo('Authors', [
  1131. 'conditions' => ['Authors.name !=' => 'mariano'],
  1132. ]);
  1133. $rules = $table->rulesChecker();
  1134. $rules->add($rules->existsIn('author_id', 'Authors'));
  1135. $this->assertFalse($table->save($entity));
  1136. $this->assertEquals(['_existsIn' => 'This value does not exist'], $entity->getError('author_id'));
  1137. }
  1138. /**
  1139. * Tests that associated items have a count of X.
  1140. */
  1141. public function testCountOfAssociatedItems(): void
  1142. {
  1143. $entity = new Entity([
  1144. 'title' => 'A Title',
  1145. 'body' => 'A body',
  1146. ]);
  1147. $entity->tags = [
  1148. new Entity([
  1149. 'name' => 'Something New',
  1150. ]),
  1151. new Entity([
  1152. 'name' => '100',
  1153. ]),
  1154. ];
  1155. $this->getTableLocator()->get('ArticlesTags');
  1156. $table = $this->getTableLocator()->get('articles');
  1157. $table->belongsToMany('tags');
  1158. $rules = $table->rulesChecker();
  1159. $rules->add($rules->validCount('tags', 3));
  1160. $this->assertFalse($table->save($entity));
  1161. $this->assertEquals($entity->getErrors(), [
  1162. 'tags' => [
  1163. '_validCount' => 'The count does not match >3',
  1164. ],
  1165. ]);
  1166. // Testing that undesired types fail
  1167. $entity->tags = null;
  1168. $this->assertFalse($table->save($entity));
  1169. $entity->tags = new stdClass();
  1170. $this->assertFalse($table->save($entity));
  1171. $entity->tags = 'string';
  1172. $this->assertFalse($table->save($entity));
  1173. $entity->tags = 123456;
  1174. $this->assertFalse($table->save($entity));
  1175. $entity->tags = 0.512;
  1176. $this->assertFalse($table->save($entity));
  1177. }
  1178. /**
  1179. * Tests that the error field name is inferred from the association name in case no name is provided.
  1180. */
  1181. public function testIsLinkedToInferFieldFromAssociationName(): void
  1182. {
  1183. $Comments = $this->getTableLocator()->get('Comments');
  1184. $Comments->belongsTo('Articles');
  1185. $comment = $Comments->save($Comments->newEntity([
  1186. 'article_id' => 9999,
  1187. 'user_id' => 1,
  1188. 'comment' => 'Orphaned Comment',
  1189. ]));
  1190. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1191. $rulesChecker = $Comments->rulesChecker();
  1192. $rulesChecker->addUpdate(
  1193. $rulesChecker->isLinkedTo('Articles')
  1194. );
  1195. $comment->setDirty('comment', true);
  1196. $this->assertFalse($Comments->save($comment));
  1197. $expected = [
  1198. 'article' => [
  1199. '_isLinkedTo' => 'Cannot modify row: a constraint for the `Articles` association fails.',
  1200. ],
  1201. ];
  1202. $this->assertEquals($expected, $comment->getErrors());
  1203. }
  1204. /**
  1205. * Tests that the error field name is inferred from the association name in case no name is provided.
  1206. */
  1207. public function testIsNotLinkedToInferFieldFromAssociationName(): void
  1208. {
  1209. $Articles = $this->getTableLocator()->get('Articles');
  1210. $Articles->hasMany('Comments');
  1211. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1212. $rulesChecker = $Articles->rulesChecker();
  1213. $rulesChecker->addDelete(
  1214. $rulesChecker->isNotLinkedTo('Comments')
  1215. );
  1216. $article = $Articles->get(1);
  1217. $this->assertFalse($Articles->delete($article));
  1218. $expected = [
  1219. 'comments' => [
  1220. '_isNotLinkedTo' => 'Cannot modify row: a constraint for the `Comments` association fails.',
  1221. ],
  1222. ];
  1223. $this->assertEquals($expected, $article->getErrors());
  1224. }
  1225. /**
  1226. * Tests that the error field name is inferred from the association name in case no name is provided,
  1227. * and no repository is available at the time of creating the rule.
  1228. */
  1229. public function testIsLinkedToInferFieldFromAssociationNameWithNoRepositoryAvailable(): void
  1230. {
  1231. $rulesChecker = new RulesChecker();
  1232. /** @var \Cake\ORM\Table|\PHPUnit\Framework\MockObject\MockObject $Comments */
  1233. $Comments = $this->getMockForModel('Comments', ['rulesChecker'], ['className' => Table::class]);
  1234. $Comments
  1235. ->expects($this->any())
  1236. ->method('rulesChecker')
  1237. ->willReturn($rulesChecker);
  1238. $Comments->belongsTo('Articles');
  1239. $comment = $Comments->save($Comments->newEntity([
  1240. 'article_id' => 9999,
  1241. 'user_id' => 1,
  1242. 'comment' => 'Orphaned Comment',
  1243. ]));
  1244. $rulesChecker->addUpdate(
  1245. $rulesChecker->isLinkedTo('Articles'),
  1246. ['repository' => $Comments]
  1247. );
  1248. $comment->setDirty('comment', true);
  1249. $this->assertFalse($Comments->save($comment));
  1250. $expected = [
  1251. 'articles' => [
  1252. '_isLinkedTo' => 'Cannot modify row: a constraint for the `Articles` association fails.',
  1253. ],
  1254. ];
  1255. $this->assertEquals($expected, $comment->getErrors());
  1256. }
  1257. /**
  1258. * Tests that the error field name is inferred from the association name in case no name is provided,
  1259. * and no repository is available at the time of creating the rule.
  1260. */
  1261. public function testIsNotLinkedToInferFieldFromAssociationNameWithNoRepositoryAvailable(): void
  1262. {
  1263. $rulesChecker = new RulesChecker();
  1264. /** @var \Cake\ORM\Table&\PHPUnit\Framework\MockObject\MockObject $Articles */
  1265. $Articles = $this->getMockForModel('Articles', ['rulesChecker'], ['className' => Table::class]);
  1266. $Articles
  1267. ->expects($this->any())
  1268. ->method('rulesChecker')
  1269. ->willReturn($rulesChecker);
  1270. $Articles->hasMany('Comments');
  1271. $rulesChecker->addDelete(
  1272. $rulesChecker->isNotLinkedTo('Comments'),
  1273. ['repository' => $Articles]
  1274. );
  1275. $article = $Articles->get(1);
  1276. $this->assertFalse($Articles->delete($article));
  1277. $expected = [
  1278. 'comments' => [
  1279. '_isNotLinkedTo' => 'Cannot modify row: a constraint for the `Comments` association fails.',
  1280. ],
  1281. ];
  1282. $this->assertEquals($expected, $article->getErrors());
  1283. }
  1284. /**
  1285. * Tests that the error field name is inferred from the association object in case no name is provided.
  1286. */
  1287. public function testIsLinkedToInferFieldFromAssociationObject(): void
  1288. {
  1289. $Comments = $this->getTableLocator()->get('Comments');
  1290. $Comments->belongsTo('Articles');
  1291. $comment = $Comments->save($Comments->newEntity([
  1292. 'article_id' => 9999,
  1293. 'user_id' => 1,
  1294. 'comment' => 'Orphaned Comment',
  1295. ]));
  1296. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1297. $rulesChecker = $Comments->rulesChecker();
  1298. $rulesChecker->addUpdate(
  1299. $rulesChecker->isLinkedTo($Comments->getAssociation('Articles'))
  1300. );
  1301. $comment->setDirty('comment', true);
  1302. $this->assertFalse($Comments->save($comment));
  1303. $expected = [
  1304. 'article' => [
  1305. '_isLinkedTo' => 'Cannot modify row: a constraint for the `Articles` association fails.',
  1306. ],
  1307. ];
  1308. $this->assertEquals($expected, $comment->getErrors());
  1309. }
  1310. /**
  1311. * Tests that the error field name is inferred from the association object in case no name is provided.
  1312. */
  1313. public function testIsNotLinkedToInferFieldFromAssociationObject(): void
  1314. {
  1315. $Articles = $this->getTableLocator()->get('Articles');
  1316. $Articles->hasMany('Comments');
  1317. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1318. $rulesChecker = $Articles->rulesChecker();
  1319. $rulesChecker->addDelete(
  1320. $rulesChecker->isNotLinkedTo($Articles->getAssociation('Comments'))
  1321. );
  1322. $article = $Articles->get(1);
  1323. $this->assertFalse($Articles->delete($article));
  1324. $expected = [
  1325. 'comments' => [
  1326. '_isNotLinkedTo' => 'Cannot modify row: a constraint for the `Comments` association fails.',
  1327. ],
  1328. ];
  1329. $this->assertEquals($expected, $article->getErrors());
  1330. }
  1331. /**
  1332. * Tests that the custom error field name is being used.
  1333. */
  1334. public function testIsLinkedToWithCustomField(): void
  1335. {
  1336. $Comments = $this->getTableLocator()->get('Comments');
  1337. $Comments->belongsTo('Articles');
  1338. $comment = $Comments->save($Comments->newEntity([
  1339. 'article_id' => 9999,
  1340. 'user_id' => 1,
  1341. 'comment' => 'Orphaned Comment',
  1342. ]));
  1343. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1344. $rulesChecker = $Comments->rulesChecker();
  1345. $rulesChecker->addUpdate(
  1346. $rulesChecker->isLinkedTo('Articles', 'custom')
  1347. );
  1348. $comment->setDirty('comment', true);
  1349. $this->assertFalse($Comments->save($comment));
  1350. $expected = [
  1351. 'custom' => [
  1352. '_isLinkedTo' => 'Cannot modify row: a constraint for the `Articles` association fails.',
  1353. ],
  1354. ];
  1355. $this->assertEquals($expected, $comment->getErrors());
  1356. }
  1357. /**
  1358. * Tests that the custom error field name is being used.
  1359. */
  1360. public function testIsNotLinkedToWithCustomField(): void
  1361. {
  1362. $Articles = $this->getTableLocator()->get('Articles');
  1363. $Articles->hasMany('Comments');
  1364. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1365. $rulesChecker = $Articles->rulesChecker();
  1366. $rulesChecker->addDelete(
  1367. $rulesChecker->isNotLinkedTo('Comments', 'custom')
  1368. );
  1369. $article = $Articles->get(1);
  1370. $this->assertFalse($Articles->delete($article));
  1371. $expected = [
  1372. 'custom' => [
  1373. '_isNotLinkedTo' => 'Cannot modify row: a constraint for the `Comments` association fails.',
  1374. ],
  1375. ];
  1376. $this->assertEquals($expected, $article->getErrors());
  1377. }
  1378. /**
  1379. * Tests that the custom error message is being used.
  1380. */
  1381. public function testIsLinkedToWithCustomMessage(): void
  1382. {
  1383. $Comments = $this->getTableLocator()->get('Comments');
  1384. $Comments->belongsTo('Articles');
  1385. $comment = $Comments->save($Comments->newEntity([
  1386. 'article_id' => 9999,
  1387. 'user_id' => 1,
  1388. 'comment' => 'Orphaned Comment',
  1389. ]));
  1390. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1391. $rulesChecker = $Comments->rulesChecker();
  1392. $rulesChecker->addUpdate(
  1393. $rulesChecker->isLinkedTo('Articles', 'article', 'custom')
  1394. );
  1395. $comment->setDirty('comment', true);
  1396. $this->assertFalse($Comments->save($comment));
  1397. $expected = [
  1398. 'article' => [
  1399. '_isLinkedTo' => 'custom',
  1400. ],
  1401. ];
  1402. $this->assertEquals($expected, $comment->getErrors());
  1403. }
  1404. /**
  1405. * Tests that the custom error message is being used.
  1406. */
  1407. public function testIsNotLinkedToWithCustomMessage(): void
  1408. {
  1409. $Articles = $this->getTableLocator()->get('Articles');
  1410. $Articles->hasMany('Comments');
  1411. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1412. $rulesChecker = $Articles->rulesChecker();
  1413. $rulesChecker->addDelete(
  1414. $rulesChecker->isNotLinkedTo('Comments', 'comments', 'custom')
  1415. );
  1416. $article = $Articles->get(1);
  1417. $this->assertFalse($Articles->delete($article));
  1418. $expected = [
  1419. 'comments' => [
  1420. '_isNotLinkedTo' => 'custom',
  1421. ],
  1422. ];
  1423. $this->assertEquals($expected, $article->getErrors());
  1424. }
  1425. /**
  1426. * Tests that the default error message can be translated.
  1427. */
  1428. public function testIsLinkedToMessageWithI18n(): void
  1429. {
  1430. /** @var \Cake\I18n\Translator $translator */
  1431. $translator = I18n::getTranslator('cake');
  1432. $messageId = 'Cannot modify row: a constraint for the `{0}` association fails.';
  1433. $translator->getPackage()->addMessage(
  1434. $messageId,
  1435. 'Zeile kann nicht geändert werden: Eine Einschränkung für die "{0}" Beziehung schlägt fehl.'
  1436. );
  1437. $Comments = $this->getTableLocator()->get('Comments');
  1438. $Comments->belongsTo('Articles');
  1439. $comment = $Comments->save($Comments->newEntity([
  1440. 'article_id' => 9999,
  1441. 'user_id' => 1,
  1442. 'comment' => 'Orphaned Comment',
  1443. ]));
  1444. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1445. $rulesChecker = $Comments->rulesChecker();
  1446. $rulesChecker->addUpdate(
  1447. $rulesChecker->isLinkedTo('Articles', 'article')
  1448. );
  1449. $comment->setDirty('comment', true);
  1450. $this->assertFalse($Comments->save($comment));
  1451. $expected = [
  1452. 'article' => [
  1453. '_isLinkedTo' => 'Zeile kann nicht geändert werden: Eine Einschränkung für die "Articles" Beziehung schlägt fehl.',
  1454. ],
  1455. ];
  1456. $this->assertEquals($expected, $comment->getErrors());
  1457. $translator->getPackage()->addMessage($messageId, '');
  1458. }
  1459. /**
  1460. * Tests that the default error message can be translated.
  1461. */
  1462. public function testIsNotLinkedToMessageWithI18n(): void
  1463. {
  1464. /** @var \Cake\I18n\Translator $translator */
  1465. $translator = I18n::getTranslator('cake');
  1466. $messageId = 'Cannot modify row: a constraint for the `{0}` association fails.';
  1467. $translator->getPackage()->addMessage(
  1468. $messageId,
  1469. 'Zeile kann nicht geändert werden: Eine Einschränkung für die "{0}" Beziehung schlägt fehl.'
  1470. );
  1471. $Comments = $this->getTableLocator()->get('Comments');
  1472. $Comments->belongsTo('Articles');
  1473. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1474. $rulesChecker = $Comments->rulesChecker();
  1475. $rulesChecker->addUpdate(
  1476. $rulesChecker->isNotLinkedTo('Articles', 'articles')
  1477. );
  1478. $comment = $Comments->get(1);
  1479. $comment->setDirty('comment', true);
  1480. $this->assertFalse($Comments->save($comment));
  1481. $expected = [
  1482. 'articles' => [
  1483. '_isNotLinkedTo' => 'Zeile kann nicht geändert werden: Eine Einschränkung für die "Articles" Beziehung schlägt fehl.',
  1484. ],
  1485. ];
  1486. $this->assertEquals($expected, $comment->getErrors());
  1487. $translator->getPackage()->addMessage($messageId, '');
  1488. }
  1489. /**
  1490. * Tests that the default error message works without I18n.
  1491. */
  1492. public function testIsLinkedToMessageWithoutI18n(): void
  1493. {
  1494. /** @var \Cake\I18n\Translator $translator */
  1495. $translator = I18n::getTranslator('cake');
  1496. $messageId = 'Cannot modify row: a constraint for the `{0}` association fails.';
  1497. $translator->getPackage()->addMessage(
  1498. $messageId,
  1499. 'translated'
  1500. );
  1501. $Comments = $this->getTableLocator()->get('Comments');
  1502. $Comments->belongsTo('Articles');
  1503. $comment = $Comments->save($Comments->newEntity([
  1504. 'article_id' => 9999,
  1505. 'user_id' => 1,
  1506. 'comment' => 'Orphaned Comment',
  1507. ]));
  1508. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1509. $rulesChecker = $Comments->rulesChecker();
  1510. Closure::bind(
  1511. function () use ($rulesChecker): void {
  1512. $rulesChecker->{'_useI18n'} = false;
  1513. },
  1514. null,
  1515. RulesChecker::class
  1516. )();
  1517. $rulesChecker->addUpdate(
  1518. $rulesChecker->isLinkedTo('Articles', 'article')
  1519. );
  1520. $comment->setDirty('comment', true);
  1521. $this->assertFalse($Comments->save($comment));
  1522. $expected = [
  1523. 'article' => [
  1524. '_isLinkedTo' => 'Cannot modify row: a constraint for the `Articles` association fails.',
  1525. ],
  1526. ];
  1527. $this->assertEquals($expected, $comment->getErrors());
  1528. $translator->getPackage()->addMessage($messageId, '');
  1529. }
  1530. /**
  1531. * Tests that the default error message works without I18n.
  1532. */
  1533. public function testIsNotLinkedToMessageWithoutI18n(): void
  1534. {
  1535. /** @var \Cake\I18n\Translator $translator */
  1536. $translator = I18n::getTranslator('cake');
  1537. $messageId = 'Cannot modify row: a constraint for the `{0}` association fails.';
  1538. $translator->getPackage()->addMessage(
  1539. $messageId,
  1540. 'translated'
  1541. );
  1542. $Comments = $this->getTableLocator()->get('Comments');
  1543. $Comments->belongsTo('Articles');
  1544. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1545. $rulesChecker = $Comments->rulesChecker();
  1546. Closure::bind(
  1547. function () use ($rulesChecker): void {
  1548. $rulesChecker->{'_useI18n'} = false;
  1549. },
  1550. null,
  1551. RulesChecker::class
  1552. )();
  1553. $rulesChecker->addUpdate(
  1554. $rulesChecker->isNotLinkedTo('Articles', 'articles')
  1555. );
  1556. $comment = $Comments->get(1);
  1557. $comment->setDirty('comment', true);
  1558. $this->assertFalse($Comments->save($comment));
  1559. $expected = [
  1560. 'articles' => [
  1561. '_isNotLinkedTo' => 'Cannot modify row: a constraint for the `Articles` association fails.',
  1562. ],
  1563. ];
  1564. $this->assertEquals($expected, $comment->getErrors());
  1565. $translator->getPackage()->addMessage($messageId, '');
  1566. }
  1567. /**
  1568. * Tests that the rule can pass.
  1569. */
  1570. public function testIsLinkedToIsLinked(): void
  1571. {
  1572. $Comments = $this->getTableLocator()->get('Comments');
  1573. $Comments->belongsTo('Articles');
  1574. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1575. $rulesChecker = $Comments->rulesChecker();
  1576. $rulesChecker->addUpdate(
  1577. $rulesChecker->isLinkedTo('Articles', 'articles')
  1578. );
  1579. $comment = $Comments->get(1);
  1580. $comment->setDirty('comment', true);
  1581. $this->assertNotFalse($Comments->save($comment));
  1582. }
  1583. /**
  1584. * Tests that the rule can pass.
  1585. */
  1586. public function testIsNotLinkedToIsNotLinked(): void
  1587. {
  1588. $Articles = $this->getTableLocator()->get('Articles');
  1589. $Articles->hasMany('Comments');
  1590. /** @var \Cake\ORM\RulesChecker $rulesChecker */
  1591. $rulesChecker = $Articles->rulesChecker();
  1592. $rulesChecker->addDelete(
  1593. $rulesChecker->isNotLinkedTo('Comments', 'comments')
  1594. );
  1595. $article = $Articles->get(3);
  1596. $this->assertTrue($Articles->delete($article));
  1597. }
  1598. }