RulesCheckerIntegrationTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604
  1. <?php
  2. /**
  3. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  4. * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  5. *
  6. * Licensed under The MIT License
  7. * For full copyright and license information, please see the LICENSE.txt
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. * @link http://cakephp.org CakePHP(tm) Project
  12. * @since 3.0.0
  13. * @license http://www.opensource.org/licenses/mit-license.php MIT License
  14. */
  15. namespace Cake\Test\TestCase\ORM;
  16. use Cake\ORM\Entity;
  17. use Cake\ORM\RulesChecker;
  18. use Cake\ORM\TableRegistry;
  19. use Cake\TestSuite\TestCase;
  20. /**
  21. * Tests the integration between the ORM and the domain checker
  22. */
  23. class RulesCheckerIntegrationTest extends TestCase {
  24. /**
  25. * Fixtures to be loaded
  26. *
  27. * @var array
  28. */
  29. public $fixtures = ['core.articles', 'core.authors', 'core.tags', 'core.articles_tags'];
  30. /**
  31. * Tear down
  32. *
  33. * @return void
  34. */
  35. public function tearDown() {
  36. parent::tearDown();
  37. TableRegistry::clear();
  38. }
  39. /**
  40. * Tests saving belongsTo association and get a validation error
  41. *
  42. * @group save
  43. * @return void
  44. */
  45. public function testsSaveBelongsToWithValidationError() {
  46. $entity = new Entity([
  47. 'title' => 'A Title',
  48. 'body' => 'A body'
  49. ]);
  50. $entity->author = new Entity([
  51. 'name' => 'Jose'
  52. ]);
  53. $table = TableRegistry::get('articles');
  54. $table->belongsTo('authors');
  55. $table->association('authors')
  56. ->target()
  57. ->rulesChecker()
  58. ->add(function (Entity $author, array $options) use ($table) {
  59. $this->assertSame($options['repository'], $table->association('authors')->target());
  60. return false;
  61. }, ['errorField' => 'name', 'message' => 'This is an error']);
  62. $this->assertFalse($table->save($entity));
  63. $this->assertTrue($entity->isNew());
  64. $this->assertTrue($entity->author->isNew());
  65. $this->assertNull($entity->get('author_id'));
  66. $this->assertNotEmpty($entity->author->errors('name'));
  67. $this->assertEquals(['This is an error'], $entity->author->errors('name'));
  68. }
  69. /**
  70. * Tests saving hasOne association and returning a validation error will
  71. * abort the saving process
  72. *
  73. * @group save
  74. * @return void
  75. */
  76. public function testSaveHasOneWithValidationError() {
  77. $entity = new \Cake\ORM\Entity([
  78. 'name' => 'Jose'
  79. ]);
  80. $entity->article = new \Cake\ORM\Entity([
  81. 'title' => 'A Title',
  82. 'body' => 'A body'
  83. ]);
  84. $table = TableRegistry::get('authors');
  85. $table->hasOne('articles');
  86. $table->association('articles')
  87. ->target()
  88. ->rulesChecker()
  89. ->add(function (Entity $entity) {
  90. return false;
  91. }, ['errorField' => 'title', 'message' => 'This is an error']);
  92. $this->assertFalse($table->save($entity));
  93. $this->assertTrue($entity->isNew());
  94. $this->assertTrue($entity->article->isNew());
  95. $this->assertNull($entity->article->id);
  96. $this->assertNull($entity->article->get('author_id'));
  97. $this->assertFalse($entity->article->dirty('author_id'));
  98. $this->assertNotEmpty($entity->article->errors('title'));
  99. }
  100. /**
  101. * Tests saving multiple entities in a hasMany association and getting and
  102. * error while saving one of them. It should abort all the save operation
  103. * when options are set to defaults
  104. *
  105. * @return void
  106. */
  107. public function testSaveHasManyWithErrorsAtomic() {
  108. $entity = new \Cake\ORM\Entity([
  109. 'name' => 'Jose'
  110. ]);
  111. $entity->articles = [
  112. new \Cake\ORM\Entity([
  113. 'title' => '1',
  114. 'body' => 'A body'
  115. ]),
  116. new \Cake\ORM\Entity([
  117. 'title' => 'Another Title',
  118. 'body' => 'Another body'
  119. ])
  120. ];
  121. $table = TableRegistry::get('authors');
  122. $table->hasMany('articles');
  123. $table->association('articles')
  124. ->target()
  125. ->rulesChecker()
  126. ->add(function (Entity $entity, $options) use ($table) {
  127. $this->assertSame($table, $options['_sourceTable']);
  128. return $entity->title === '1';
  129. }, ['errorField' => 'title', 'message' => 'This is an error']);
  130. $this->assertFalse($table->save($entity));
  131. $this->assertTrue($entity->isNew());
  132. $this->assertTrue($entity->articles[0]->isNew());
  133. $this->assertTrue($entity->articles[1]->isNew());
  134. $this->assertNull($entity->articles[0]->id);
  135. $this->assertNull($entity->articles[1]->id);
  136. $this->assertNull($entity->articles[0]->author_id);
  137. $this->assertNull($entity->articles[1]->author_id);
  138. $this->assertEmpty($entity->articles[0]->errors());
  139. $this->assertNotEmpty($entity->articles[1]->errors());
  140. }
  141. /**
  142. * Tests that it is possible to continue saving hasMany associations
  143. * even if any of the records fail validation when atomic is set
  144. * to false
  145. *
  146. * @return void
  147. */
  148. public function testSaveHasManyWithErrorsNonAtomic() {
  149. $entity = new \Cake\ORM\Entity([
  150. 'name' => 'Jose'
  151. ]);
  152. $entity->articles = [
  153. new \Cake\ORM\Entity([
  154. 'title' => 'A title',
  155. 'body' => 'A body'
  156. ]),
  157. new \Cake\ORM\Entity([
  158. 'title' => '1',
  159. 'body' => 'Another body'
  160. ])
  161. ];
  162. $table = TableRegistry::get('authors');
  163. $table->hasMany('articles');
  164. $table->association('articles')
  165. ->target()
  166. ->rulesChecker()
  167. ->add(function (Entity $article) {
  168. return is_numeric($article->title);
  169. }, ['errorField' => 'title', 'message' => 'This is an error']);
  170. $result = $table->save($entity, ['atomic' => false]);
  171. $this->assertSame($entity, $result);
  172. $this->assertFalse($entity->isNew());
  173. $this->assertTrue($entity->articles[0]->isNew());
  174. $this->assertFalse($entity->articles[1]->isNew());
  175. $this->assertEquals(4, $entity->articles[1]->id);
  176. $this->assertNull($entity->articles[0]->id);
  177. $this->assertNotEmpty($entity->articles[0]->errors('title'));
  178. }
  179. /**
  180. * Tests saving belongsToMany records with a validation error in a joint entity
  181. *
  182. * @group save
  183. * @return void
  184. */
  185. public function testSaveBelongsToManyWithValidationErrorInJointEntity() {
  186. $entity = new \Cake\ORM\Entity([
  187. 'title' => 'A Title',
  188. 'body' => 'A body'
  189. ]);
  190. $entity->tags = [
  191. new \Cake\ORM\Entity([
  192. 'name' => 'Something New'
  193. ]),
  194. new \Cake\ORM\Entity([
  195. 'name' => '100'
  196. ])
  197. ];
  198. $table = TableRegistry::get('articles');
  199. $table->belongsToMany('tags');
  200. $table->association('tags')
  201. ->junction()
  202. ->rulesChecker()
  203. ->add(function (Entity $entity) {
  204. return $entity->article_id > 4;
  205. });
  206. $this->assertFalse($table->save($entity));
  207. $this->assertTrue($entity->isNew());
  208. $this->assertTrue($entity->tags[0]->isNew());
  209. $this->assertTrue($entity->tags[1]->isNew());
  210. $this->assertNull($entity->tags[0]->id);
  211. $this->assertNull($entity->tags[1]->id);
  212. $this->assertNull($entity->tags[0]->_joinData);
  213. $this->assertNull($entity->tags[1]->_joinData);
  214. }
  215. /**
  216. * Tests saving belongsToMany records with a validation error in a joint entity
  217. * and atomic set to false
  218. *
  219. * @group save
  220. * @return void
  221. */
  222. public function testSaveBelongsToManyWithValidationErrorInJointEntityNonAtomic() {
  223. $entity = new \Cake\ORM\Entity([
  224. 'title' => 'A Title',
  225. 'body' => 'A body'
  226. ]);
  227. $entity->tags = [
  228. new \Cake\ORM\Entity([
  229. 'name' => 'Something New'
  230. ]),
  231. new \Cake\ORM\Entity([
  232. 'name' => 'New one'
  233. ])
  234. ];
  235. $table = TableRegistry::get('articles');
  236. $table->belongsToMany('tags');
  237. $table->association('tags')
  238. ->junction()
  239. ->rulesChecker()
  240. ->add(function (Entity $entity) {
  241. return $entity->tag_id > 4;
  242. });
  243. $this->assertSame($entity, $table->save($entity, ['atomic' => false]));
  244. $this->assertFalse($entity->isNew());
  245. $this->assertFalse($entity->tags[0]->isNew());
  246. $this->assertFalse($entity->tags[1]->isNew());
  247. $this->assertEquals(4, $entity->tags[0]->id);
  248. $this->assertEquals(5, $entity->tags[1]->id);
  249. $this->assertTrue($entity->tags[0]->_joinData->isNew());
  250. $this->assertEquals(4, $entity->tags[1]->_joinData->article_id);
  251. $this->assertEquals(5, $entity->tags[1]->_joinData->tag_id);
  252. }
  253. /**
  254. * Tests the isUnique domain rule
  255. *
  256. * @group save
  257. * @return void
  258. */
  259. public function testIsUniqueDomainRule() {
  260. $entity = new Entity([
  261. 'name' => 'larry'
  262. ]);
  263. $table = TableRegistry::get('Authors');
  264. $rules = $table->rulesChecker();
  265. $rules->add($rules->isUnique(['name']));
  266. $this->assertFalse($table->save($entity));
  267. $this->assertEquals(['This value is already in use'], $entity->errors('name'));
  268. $entity->name = 'jose';
  269. $this->assertSame($entity, $table->save($entity));
  270. $entity = $table->get(1);
  271. $entity->dirty('name', true);
  272. $this->assertSame($entity, $table->save($entity));
  273. }
  274. /**
  275. * Tests isUnique with multiple fields
  276. *
  277. * @group save
  278. * @return void
  279. */
  280. public function testIsUniqueMultipleFields() {
  281. $entity = new Entity([
  282. 'author_id' => 1,
  283. 'title' => 'First Article'
  284. ]);
  285. $table = TableRegistry::get('Articles');
  286. $rules = $table->rulesChecker();
  287. $rules->add($rules->isUnique(['title', 'author_id'], 'Nope'));
  288. $this->assertFalse($table->save($entity));
  289. $this->assertEquals(['title' => ['Nope']], $entity->errors());
  290. $entity->clean();
  291. $entity->author_id = 2;
  292. $this->assertSame($entity, $table->save($entity));
  293. }
  294. /**
  295. * Tests the existsIn domain rule
  296. *
  297. * @group save
  298. * @return void
  299. */
  300. public function testExistsInDomainRule() {
  301. $entity = new Entity([
  302. 'title' => 'An Article',
  303. 'author_id' => 500
  304. ]);
  305. $table = TableRegistry::get('Articles');
  306. $table->belongsTo('Authors');
  307. $rules = $table->rulesChecker();
  308. $rules->add($rules->existsIn('author_id', 'Authors'));
  309. $this->assertFalse($table->save($entity));
  310. $this->assertEquals(['This value does not exist'], $entity->errors('author_id'));
  311. }
  312. /**
  313. * Tests the existsIn domain rule when passing an object
  314. *
  315. * @group save
  316. * @return void
  317. */
  318. public function testExistsInDomainRuleWithObject() {
  319. $entity = new Entity([
  320. 'title' => 'An Article',
  321. 'author_id' => 500
  322. ]);
  323. $table = TableRegistry::get('Articles');
  324. $rules = $table->rulesChecker();
  325. $rules->add($rules->existsIn('author_id', TableRegistry::get('Authors'), 'Nope'));
  326. $this->assertFalse($table->save($entity));
  327. $this->assertEquals(['Nope'], $entity->errors('author_id'));
  328. }
  329. /**
  330. * Tests the checkRules save option
  331. *
  332. * @group save
  333. * @return void
  334. */
  335. public function testSkipRulesChecking() {
  336. $entity = new Entity([
  337. 'title' => 'An Article',
  338. 'author_id' => 500
  339. ]);
  340. $table = TableRegistry::get('Articles');
  341. $rules = $table->rulesChecker();
  342. $rules->add($rules->existsIn('author_id', TableRegistry::get('Authors'), 'Nope'));
  343. $this->assertSame($entity, $table->save($entity, ['checkRules' => false]));
  344. }
  345. /**
  346. * Tests the beforeRules event
  347. *
  348. * @group save
  349. * @return void
  350. */
  351. public function testUseBeforeRules() {
  352. $entity = new Entity([
  353. 'title' => 'An Article',
  354. 'author_id' => 500
  355. ]);
  356. $table = TableRegistry::get('Articles');
  357. $rules = $table->rulesChecker();
  358. $rules->add($rules->existsIn('author_id', TableRegistry::get('Authors'), 'Nope'));
  359. $table->eventManager()->attach(
  360. function ($event, Entity $entity, \ArrayObject $options, $operation) {
  361. $this->assertEquals(
  362. ['atomic' => true, 'associated' => true, 'checkRules' => true],
  363. $options->getArrayCopy()
  364. );
  365. $this->assertEquals('create', $operation);
  366. $event->stopPropagation();
  367. return true;
  368. },
  369. 'Model.beforeRules'
  370. );
  371. $this->assertSame($entity, $table->save($entity));
  372. }
  373. /**
  374. * Tests the afterRules event
  375. *
  376. * @group save
  377. * @return void
  378. */
  379. public function testUseAfterRules() {
  380. $entity = new Entity([
  381. 'title' => 'An Article',
  382. 'author_id' => 500
  383. ]);
  384. $table = TableRegistry::get('Articles');
  385. $rules = $table->rulesChecker();
  386. $rules->add($rules->existsIn('author_id', TableRegistry::get('Authors'), 'Nope'));
  387. $table->eventManager()->attach(
  388. function ($event, Entity $entity, \ArrayObject $options, $result, $operation) {
  389. $this->assertEquals(
  390. ['atomic' => true, 'associated' => true, 'checkRules' => true],
  391. $options->getArrayCopy()
  392. );
  393. $this->assertEquals('create', $operation);
  394. $this->assertFalse($result);
  395. $event->stopPropagation();
  396. return true;
  397. },
  398. 'Model.afterRules'
  399. );
  400. $this->assertSame($entity, $table->save($entity));
  401. }
  402. /**
  403. * Tests that rules can be changed using the buildRules event
  404. *
  405. * @group save
  406. * @return void
  407. */
  408. public function testUseBuildRulesEvent() {
  409. $entity = new Entity([
  410. 'title' => 'An Article',
  411. 'author_id' => 500
  412. ]);
  413. $table = TableRegistry::get('Articles');
  414. $table->eventManager()->attach(function ($event, $rules) {
  415. $rules->add($rules->existsIn('author_id', TableRegistry::get('Authors'), 'Nope'));
  416. }, 'Model.buildRules');
  417. $this->assertFalse($table->save($entity));
  418. }
  419. /**
  420. * Tests isUnique with untouched fields
  421. *
  422. * @group save
  423. * @return void
  424. */
  425. public function testIsUniqueWithCleanFields() {
  426. $table = TableRegistry::get('Articles');
  427. $entity = $table->get(1);
  428. $rules = $table->rulesChecker();
  429. $rules->add($rules->isUnique(['title', 'author_id'], 'Nope'));
  430. $entity->body = 'Foo';
  431. $this->assertSame($entity, $table->save($entity));
  432. $entity->title = 'Third Article';
  433. $this->assertFalse($table->save($entity));
  434. }
  435. /**
  436. * Tests the existsIn rule when passing non dirty fields
  437. *
  438. * @group save
  439. * @return void
  440. */
  441. public function testExistsInWithCleanFields() {
  442. $table = TableRegistry::get('Articles');
  443. $table->belongsTo('Authors');
  444. $rules = $table->rulesChecker();
  445. $rules->add($rules->existsIn('author_id', 'Authors'));
  446. $entity = $table->get(1);
  447. $entity->title = 'Foo';
  448. $entity->author_id = 1000;
  449. $entity->dirty('author_id', false);
  450. $this->assertSame($entity, $table->save($entity));
  451. }
  452. /**
  453. * Tests using rules to prevent delete operations
  454. *
  455. * @group delete
  456. * @return void
  457. */
  458. public function testDeleteRules() {
  459. $table = TableRegistry::get('Articles');
  460. $rules = $table->rulesChecker();
  461. $rules->addDelete(function ($entity) {
  462. return false;
  463. });
  464. $entity = $table->get(1);
  465. $this->assertFalse($table->delete($entity));
  466. }
  467. /**
  468. * Checks that it is possible to pass custom options to rules when saving
  469. *
  470. * @group save
  471. * @return void
  472. */
  473. public function testCustomOptionsPassingSave() {
  474. $entity = new Entity([
  475. 'name' => 'jose'
  476. ]);
  477. $table = TableRegistry::get('Authors');
  478. $rules = $table->rulesChecker();
  479. $rules->add(function ($entity, $options) {
  480. $this->assertEquals('bar', $options['foo']);
  481. $this->assertEquals('option', $options['another']);
  482. return false;
  483. }, ['another' => 'option']);
  484. $this->assertFalse($table->save($entity, ['foo' => 'bar']));
  485. }
  486. /**
  487. * Tests passing custom options to rules from delete
  488. *
  489. * @group delete
  490. * @return void
  491. */
  492. public function testCustomOptionsPassingDelete() {
  493. $table = TableRegistry::get('Articles');
  494. $rules = $table->rulesChecker();
  495. $rules->addDelete(function ($entity, $options) {
  496. $this->assertEquals('bar', $options['foo']);
  497. $this->assertEquals('option', $options['another']);
  498. return false;
  499. }, ['another' => 'option']);
  500. $entity = $table->get(1);
  501. $this->assertFalse($table->delete($entity, ['foo' => 'bar']));
  502. }
  503. /**
  504. * Tests that using existsIn for a hasMany association will not be called
  505. * as the foreign key for the association was automatically validated already.
  506. *
  507. * @group save
  508. * @return void
  509. */
  510. public function testAvoidExistsInOnAutomaticSaving() {
  511. $entity = new \Cake\ORM\Entity([
  512. 'name' => 'Jose'
  513. ]);
  514. $entity->articles = [
  515. new \Cake\ORM\Entity([
  516. 'title' => '1',
  517. 'body' => 'A body'
  518. ]),
  519. new \Cake\ORM\Entity([
  520. 'title' => 'Another Title',
  521. 'body' => 'Another body'
  522. ])
  523. ];
  524. $table = TableRegistry::get('authors');
  525. $table->hasMany('articles');
  526. $table->association('articles')->belongsTo('authors');
  527. $checker = $table->association('articles')->target()->rulesChecker();
  528. $checker->add(function ($entity, $options) use ($checker) {
  529. $rule = $checker->existsIn('author_id', 'authors');
  530. $id = $entity->author_id;
  531. $entity->author_id = 5000;
  532. $result = $rule($entity, $options);
  533. $this->assertTrue($result);
  534. $entity->author_id = $id;
  535. return true;
  536. });
  537. $this->assertSame($entity, $table->save($entity));
  538. }
  539. }