RulesCheckerIntegrationTest.php 61 KB

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