diff --git a/public/vendor/exment/js/scroll-restore.js b/public/vendor/exment/js/scroll-restore.js
new file mode 100644
index 000000000..368ca822b
--- /dev/null
+++ b/public/vendor/exment/js/scroll-restore.js
@@ -0,0 +1,128 @@
+/**
+ * Scroll position restore for custom table list view
+ * Handles scroll restoration on browser back button and pjax navigation
+ */
+var Exment;
+(function (Exment) {
+ 'use strict';
+
+ const CLASSNAME_CUSTOM_VALUE_GRID = '.block_custom_value_grid';
+
+ // Save current scroll position
+ function saveCurrentScrollPosition() {
+ const scrollTop = $(window).scrollTop() || document.documentElement.scrollTop || document.body.scrollTop || 0;
+
+ // Store in history state if available
+ if (window.history && window.history.replaceState) {
+ const currentState = window.history.state || {};
+ currentState.scrollTop = scrollTop;
+ try {
+ window.history.replaceState(currentState, document.title);
+ } catch (e) {
+ console.warn('Failed to update history state:', e);
+ }
+ }
+ }
+
+ // Restore scroll position from history state
+ function restoreScrollPosition() {
+ let scrollTop = 0;
+
+ // Only restore from history state (for back button)
+ if (window.history && window.history.state && typeof window.history.state.scrollTop === 'number') {
+ scrollTop = window.history.state.scrollTop;
+ }
+
+ if (scrollTop > 0) {
+ // Use setTimeout to ensure DOM is ready
+ setTimeout(function() {
+ window.scrollTo(0, scrollTop);
+ // Verify and retry if needed
+ setTimeout(function() {
+ const currentScroll = window.pageYOffset || document.documentElement.scrollTop;
+ if (Math.abs(currentScroll - scrollTop) > 50) {
+ window.scrollTo(0, scrollTop);
+ }
+ }, 100);
+ }, 0);
+ }
+ }
+
+ // ScrollRestore module
+ Exment.ScrollRestore = {
+ init: function() {
+ // Prevent multiple initialization
+ if (window.exmentScrollRestoreInitialized) {
+ return;
+ }
+ window.exmentScrollRestoreInitialized = true;
+
+ // Disable browser's default scroll restoration
+ if ('scrollRestoration' in history) {
+ history.scrollRestoration = 'manual';
+ }
+
+ restoreScrollPosition();
+
+ // Save scroll position periodically while scrolling
+ let scrollTimer;
+ $(window).on('scroll', function() {
+ clearTimeout(scrollTimer);
+ scrollTimer = setTimeout(function() {
+ // Only save if we're on a list page (grid view)
+ if ($(CLASSNAME_CUSTOM_VALUE_GRID).length > 0) {
+ saveCurrentScrollPosition();
+ }
+ }, 150);
+ });
+
+ // Save scroll position before leaving page
+ $(window).on('beforeunload', function() {
+ if ($(CLASSNAME_CUSTOM_VALUE_GRID).length > 0) {
+ saveCurrentScrollPosition();
+ }
+ });
+
+ // Handle pjax events
+ if ($.pjax) {
+ let isPjaxPopstate = false;
+
+ $(document).on('pjax:popstate', function() {
+ isPjaxPopstate = true;
+ });
+
+ // Save scroll position before pjax request
+ $(document).on('pjax:send', function(event, xhr, options) {
+ if ($(CLASSNAME_CUSTOM_VALUE_GRID).length > 0) {
+ saveCurrentScrollPosition();
+ }
+ });
+
+ // Handle scroll position after pjax complete
+ $(document).on('pjax:end', function(event, xhr, options) {
+ if ($(CLASSNAME_CUSTOM_VALUE_GRID).length > 0) {
+ if (isPjaxPopstate) {
+ // If it's a back/forward navigation, restore scroll
+ restoreScrollPosition();
+ isPjaxPopstate = false;
+ } else {
+ // If it's a new navigation (pagination, sort, etc), scroll to top
+ window.scrollTo(0, 0);
+ }
+ }
+ });
+ }
+
+ // Save position when clicking on links in grid
+ $(CLASSNAME_CUSTOM_VALUE_GRID).on('click', 'a', function() {
+ saveCurrentScrollPosition();
+ });
+ }
+ };
+
+ // Auto-initialize on document ready
+ $(function() {
+ Exment.ScrollRestore.init();
+ });
+
+})(Exment || (Exment = {}));
diff --git a/resources/lang/en/exment.php b/resources/lang/en/exment.php
index 3b959a6c3..84d1386cc 100644
--- a/resources/lang/en/exment.php
+++ b/resources/lang/en/exment.php
@@ -1196,6 +1196,7 @@
'all_user_viewable_flg' => 'All Users Can View',
'all_user_accessable_flg' => 'All Users Can Access',
'inherit_parent_permission' => 'Viewable with Permissions of Parent data',
+ 'editable_with_parent' => 'Need Edit Permission for Parend data',
'add_parent_menu_flg' => 'Add Menu',
'add_notify_flg' => 'Add To Notification',
'add_parent_menu' => 'Target Parent Menu',
@@ -1278,6 +1279,7 @@
'all_user_viewable_flg' => 'If set to YES, all users will be able to view all the data in this table.',
'all_user_accessable_flg' => 'If set to YES, all users will be able to see all the data in this table.
*It is not displayed on the menu or list page, it can be displayed only with internal data or reference from another table.',
'inherit_parent_permission' => 'If set to YES, the data can be viewed based on the the parent data(1:N) permissions. However, you must have viewing or similar permissions for this table itself.',
+ 'editable_with_parent' => 'If set to YES, you need edit permissions for the parent data(1:N) to edit the data in this table.',
'add_parent_menu_flg' => 'After creating custom table, you can add it to the menu. To add it, please set it to YES. *It will be displayed after updating the browser.
*It can be set only when new table is created. When updating please set it from "Menu" page.',
'add_parent_menu' => 'Please select the menu name to be parent.',
'add_notify_flg' => 'You can add settings for performing in-system notification to authorized users when creating/updating/sharing/commenting data, after creating a new table. Please add YES if you want to add.
* It can be set only when creating a new table. Please set from the "notification" page when updating.',
diff --git a/resources/lang/ja/exment.php b/resources/lang/ja/exment.php
index 713a41358..d8998a146 100644
--- a/resources/lang/ja/exment.php
+++ b/resources/lang/ja/exment.php
@@ -1197,6 +1197,7 @@
'all_user_viewable_flg' => '全ユーザーが閲覧可能',
'all_user_accessable_flg' => '全ユーザーが参照可能',
'inherit_parent_permission' => '親データの権限で閲覧可能',
+ 'editable_with_parent' => '親データの編集権限が必要',
'add_parent_menu_flg' => 'メニューに追加する',
'add_notify_flg' => '通知に追加する',
'add_parent_menu' => '追加先の親メニュー',
@@ -1279,6 +1280,7 @@
'all_user_viewable_flg' => 'YESにした場合、すべてのユーザーが、このテーブルのすべてのデータを閲覧可能になります。',
'all_user_accessable_flg' => 'YESにした場合、すべてのユーザーが、このテーブルのすべてのデータを参照可能になります。
※メニューや一覧画面では表示されず、内部データや、他のテーブルからの参照でのみ表示できます。',
'inherit_parent_permission' => 'YESにした場合、親データ(1対多)の権限でデータを閲覧できます。ただし、このテーブル自体に担当者の閲覧等の権限を持っている必要があります。',
+ 'editable_with_parent' => 'YESにした場合、このテーブルのデータを編集するには親データ(1対多)の編集権限が必要になります。',
'add_parent_menu_flg' => '新規作成後、メニューに追加することができます。追加する場合はYESにしてください。
※ブラウザ更新後に表示されます。
※テーブルの新規作成時のみ設定できます。更新時は「メニュー」画面より設定してください。',
'add_parent_menu' => '親にするメニュー名を選択してください。',
'add_notify_flg' => 'データの新規作成・更新・共有、コメント時に、権限のあるユーザーに、システム内通知を行う設定を、テーブルの新規作成後に追加することができます。追加する場合はYESにしてください。
※テーブルの新規作成時のみ設定できます。更新時は「通知」画面より設定してください。',
diff --git a/src/Controllers/ApiDataTrait.php b/src/Controllers/ApiDataTrait.php
index 76aa611ee..dbdf0f140 100644
--- a/src/Controllers/ApiDataTrait.php
+++ b/src/Controllers/ApiDataTrait.php
@@ -16,6 +16,7 @@
use Exceedone\Exment\Enums\SearchType;
use Exceedone\Exment\Enums\ValueType;
use Exceedone\Exment\Enums\ErrorCode;
+use Exceedone\Exment\Enums\RelationType;
use Validator;
/**
@@ -202,6 +203,7 @@ protected function modifyAfterGetValue(Request $request, $target, $options = [])
'id',
'target_view_id',
'children',
+ 'parents',
]),
$options['appends']
);
@@ -267,6 +269,11 @@ protected function modifyCustomValue(Request $request, $custom_value, $recursive
$custom_value = $this->modifyChildrenValue($request, $custom_value);
}
+ // Change relation key name
+ if (!$recursive && $request->has('parents') && boolval($request->get('parents'))) {
+ $custom_value = $this->modifyParentsValue($request, $custom_value);
+ }
+
// convert to custom values
$valuetype = $request->get('valuetype');
if ($request->has('valuetype') && ValueType::isRegetApiCustomValue($valuetype)) {
@@ -386,10 +393,22 @@ protected function setQueryInfo($query)
$query->with($relation->getRelationName());
}
}
+ if ($request->has('parents') && boolval($request->get('parents'))) {
+ $relations = CustomRelation::getRelationsByChild($this->custom_table);
+ foreach ($relations as $relation) {
+ $query->with($relation->getRelationName());
+ }
+ }
return $query;
}
+ /**
+ * Modify children values (exclude virtual columns from children values)
+ *
+ * @param Request $request
+ * @return CustomValue $custom_value
+ */
protected function modifyChildrenValue(Request $request, $custom_value)
{
$relations = CustomRelation::getRelationsByParent($this->custom_table);
@@ -417,4 +436,46 @@ protected function modifyChildrenValue(Request $request, $custom_value)
$custom_value['children'] = $results;
return $custom_value;
}
+
+ /**
+ * Modify parents values (exclude virtual columns from parents values)
+ *
+ * @param Request $request
+ * @return CustomValue $custom_value
+ */
+ protected function modifyParentsValue(Request $request, $custom_value)
+ {
+ $relations = CustomRelation::getRelationsByChild($this->custom_table);
+
+ $results = [];
+ foreach ($relations as $relation) {
+ // If getted relation name, change key name
+ $reltionName = $relation->getRelationName();
+ if (array_has($custom_value, $reltionName)) {
+ $relationValues = $custom_value[$reltionName];
+ $makeHiddenArray = $relation->parent_custom_table_cache->getMakeHiddenArray();
+
+ if ($relation->relation_type == RelationType::ONE_TO_MANY) {
+ // Call makehidden
+ $relationValues = $relationValues->makeHidden($makeHiddenArray);
+ // Call modify custom value
+ $relationValues = $this->modifyCustomValue($request, $relationValues, true);
+ } else {
+ $relationValues = $relationValues->map(function ($relationValue) use ($makeHiddenArray, $request) {
+ // Call makehidden
+ $relationValue = $relationValue->makeHidden($makeHiddenArray);
+ // Call modify custom value
+ $relationValue = $this->modifyCustomValue($request, $relationValue, true);
+ return $relationValue;
+ });
+ }
+ // Set key name
+ $results[$relation->parent_custom_table_cache->table_name] = $relationValues;
+ unset($custom_value[$reltionName]);
+ }
+ }
+
+ $custom_value['parents'] = $results;
+ return $custom_value;
+ }
}
diff --git a/src/Controllers/CustomTableController.php b/src/Controllers/CustomTableController.php
index ceadd0fdf..339bceede 100644
--- a/src/Controllers/CustomTableController.php
+++ b/src/Controllers/CustomTableController.php
@@ -318,6 +318,9 @@ protected function form($id = null)
if ($has_parent) {
$form->switchbool('inherit_parent_permission', exmtrans("custom_table.inherit_parent_permission"))->help(exmtrans("custom_table.help.inherit_parent_permission"))
->default("0");
+
+ $form->switchbool('editable_with_parent', exmtrans("custom_table.editable_with_parent"))->help(exmtrans("custom_table.help.editable_with_parent"))
+ ->default("1");
}
})->disableHeader();
diff --git a/src/DataItems/Grid/DefaultGrid.php b/src/DataItems/Grid/DefaultGrid.php
index 140f7584f..087ca1141 100644
--- a/src/DataItems/Grid/DefaultGrid.php
+++ b/src/DataItems/Grid/DefaultGrid.php
@@ -558,10 +558,18 @@ protected function manageRowAction($grid)
$enableCreate = false;
}
- if (!is_null($parent_value = $actions->row->getParentValue(null, true)) && $parent_value->enableEdit(true) !== true) {
- $enableCreate = false;
- $enableEdit = false;
- $enableDelete = false;
+ if (!is_null($parent_value = $actions->row->getParentValue(null, true))) {
+ if (boolval($custom_table->getOption('editable_with_parent')??1)) {
+ if ($parent_value->enableEdit(true) !== true) {
+ $enableCreate = false;
+ $enableEdit = false;
+ $enableDelete = false;
+ }
+ } elseif ($parent_value->enableAccess() !== true) {
+ $enableCreate = false;
+ $enableEdit = false;
+ $enableDelete = false;
+ }
}
if (!$enableEdit) {
diff --git a/src/DataItems/Show/DefaultShow.php b/src/DataItems/Show/DefaultShow.php
index ec99bb5fa..a3afdb806 100644
--- a/src/DataItems/Show/DefaultShow.php
+++ b/src/DataItems/Show/DefaultShow.php
@@ -170,9 +170,16 @@ public function createShowForm()
$tools->disableList();
}
- if (!is_null($parent_value = $this->custom_value->getParentValue(null, true)) && $parent_value->enableEdit(true) !== true) {
- $tools->disableEdit();
- $tools->disableDelete();
+ if (!is_null($parent_value = $this->custom_value->getParentValue(null, true))) {
+ if (boolval($this->custom_table->getOption('editable_with_parent')??1)) {
+ if ($parent_value->enableEdit(true) !== true) {
+ $tools->disableEdit();
+ $tools->disableDelete();
+ }
+ } elseif ($parent_value->enableAccess() !== true) {
+ $tools->disableEdit();
+ $tools->disableDelete();
+ }
}
if ($this->modal) {
diff --git a/src/Jobs/MailSendJob.php b/src/Jobs/MailSendJob.php
index feabdeea3..1d9dc0d33 100644
--- a/src/Jobs/MailSendJob.php
+++ b/src/Jobs/MailSendJob.php
@@ -28,13 +28,13 @@ class MailSendJob extends Notification implements ShouldQueue
* @var MailHistory
*/
protected $mailHistory;
- protected $user;
+ protected $userId;
protected $finalUser;
- public function __construct($user = null, $finalUser = false)
+ public function __construct($userId = null, $finalUser = false)
{
- $this->user = $user;
+ $this->userId = $userId;
$this->finalUser = $finalUser;
}
@@ -104,7 +104,7 @@ public function failed($exception)
$mail_template->getValue('mail_subject'),
$mail_template->getValue('mail_body'),
$this->notify_id ?? -1,
- $this->user->id,
+ $this->userId,
\Exment::getUserId() ?? null,
$this->mailHistory->getParentId(),
$this->mailHistory->getParentType()
diff --git a/src/Middleware/Bootstrap.php b/src/Middleware/Bootstrap.php
index 4c57caa8d..e15b08037 100644
--- a/src/Middleware/Bootstrap.php
+++ b/src/Middleware/Bootstrap.php
@@ -77,6 +77,7 @@ protected function setCssJs(Request $request, \Closure $next)
'vendor/exment/jstree/jstree.min.js',
'vendor/exment/js/common_all.js',
'vendor/exment/js/common.js',
+ 'vendor/exment/js/scroll-restore.js',
'vendor/exment/js/search.js',
'vendor/exment/js/calc.js',
'vendor/exment/js/notify_navbar.js',
diff --git a/src/Model/CustomValue.php b/src/Model/CustomValue.php
index 8b6718241..27c02193b 100644
--- a/src/Model/CustomValue.php
+++ b/src/Model/CustomValue.php
@@ -1794,8 +1794,17 @@ public function enableEdit($checkFormAction = false)
return ErrorCode::WORKFLOW_LOCK();
}
- if (!is_null($parent_value = $this->getParentValue()) && ($code = $parent_value->enableEdit($checkFormAction)) !== true) {
- return $code;
+ // check parent permission
+ if (!is_null($parent_value = $this->getParentValue())) {
+ if (boolval($this->custom_table->getOption('editable_with_parent')??1)) {
+ if (($code = $parent_value->enableEdit($checkFormAction)) !== true) {
+ return $code;
+ }
+ } else {
+ if (($code = $parent_value->enableAccess()) !== true) {
+ return $code;
+ }
+ }
}
if ($this->trashed()) {
@@ -1834,8 +1843,17 @@ public function enableDelete($checkFormAction = false)
return ErrorCode::DELETE_DISABLED();
}
- if (!is_null($parent_value = $this->getParentValue()) && ($code = $parent_value->enableDelete($checkFormAction)) !== true) {
- return $code;
+ // check parent permission
+ if (!is_null($parent_value = $this->getParentValue())) {
+ if (boolval($this->custom_table->getOption('editable_with_parent')??1)) {
+ if (($code = $parent_value->enableDelete($checkFormAction)) !== true) {
+ return $code;
+ }
+ } else {
+ if (($code = $parent_value->enableAccess()) !== true) {
+ return $code;
+ }
+ }
}
return true;
diff --git a/src/Notifications/MailSender.php b/src/Notifications/MailSender.php
index 2acdc66e7..2973c3dc8 100644
--- a/src/Notifications/MailSender.php
+++ b/src/Notifications/MailSender.php
@@ -241,7 +241,7 @@ protected function sendMail()
->setFromName($fromName)
->setBodyType($bodyType);
- $job = new MailSendJob(\Exment::user(), $this->final_user);
+ $job = new MailSendJob(\Exment::getUserId(), $this->final_user);
$job->setMailInfo($this->mailInfo)
->setMailHistory($this->mailHistory);
$this->notify($job);
@@ -281,7 +281,7 @@ protected function sendPasswordMail()
->setMailTemplate($mail_template)
->setHistory(false);
- $job = new MailSendJob(\Exment::user(), $this->final_user);
+ $job = new MailSendJob(\Exment::getUserId(), $this->final_user);
$job->setMailInfo($mailInfo)
->setMailHistory($mailHistory);
diff --git a/src/Services/Search/SearchService.php b/src/Services/Search/SearchService.php
index 26dc4ada6..ac9bfc676 100644
--- a/src/Services/Search/SearchService.php
+++ b/src/Services/Search/SearchService.php
@@ -466,7 +466,7 @@ protected function setSummaryOrderBy($column, $wrap_column)
{
$sort_order = array_get($column->options, 'sort_order');
if (is_nullorempty($sort_order)) {
- return $this;
+ $sort_order = 1;
}
$sort_type = isMatchString(array_get($column->options, 'sort_type'), '-1') ? 'desc' : 'asc';
diff --git a/tests/Feature/Api2Test.php b/tests/Feature/Api2Test.php
index 2618fff15..cfc4315d4 100644
--- a/tests/Feature/Api2Test.php
+++ b/tests/Feature/Api2Test.php
@@ -662,6 +662,56 @@ public function testGetValuesWithChildren2()
}
}
+ /**
+ * Getting values parents(1:N), and match ids
+ */
+ /**
+ * @return void
+ */
+ public function testGetValuesWithParents()
+ {
+ $token = $this->getAdminAccessToken([ApiScope::VALUE_READ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => "Bearer $token",
+ ])->get(admin_urls('api', 'data', TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE).'?parents=1&count=5')
+ ->assertStatus(200)
+ ->assertJsonCount(5, 'data');
+
+ // check children
+ $json = json_decode_ex($response->baseResponse->getContent(), true);
+ $data = array_get($json, 'data');
+
+ foreach ($data as $d) {
+ $this->_testParentsValues($d, TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE, array_get($d, 'id'));
+ }
+ }
+
+ /**
+ * Getting values parents(N:N), and match ids
+ */
+ /**
+ * @return void
+ */
+ public function testGetValuesWithParents2()
+ {
+ $token = $this->getAdminAccessToken([ApiScope::VALUE_READ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => "Bearer $token",
+ ])->get(admin_urls('api', 'data', TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE_MANY_TO_MANY).'?parents=1&count=5&page=2')
+ ->assertStatus(200)
+ ->assertJsonCount(5, 'data');
+
+ // check children
+ $json = json_decode_ex($response->baseResponse->getContent(), true);
+ $data = array_get($json, 'data');
+
+ foreach ($data as $d) {
+ $this->_testParentsValues($d, TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE_MANY_TO_MANY, array_get($d, 'id'));
+ }
+ }
+
/**
* @return void
*/
@@ -808,6 +858,52 @@ public function testGetValueWithChildren2()
$this->_testChildrenValues($d, TestDefine::TESTDATA_TABLE_NAME_PARENT_TABLE_MANY_TO_MANY, array_get($d, 'id'));
}
+ /**
+ * Getting value with parents, and match ids
+ */
+ /**
+ * @return void
+ */
+ public function testGetValueWithParents()
+ {
+ $token = $this->getAdminAccessToken([ApiScope::VALUE_READ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => "Bearer $token",
+ ])->get(admin_urls('api', 'data', TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE, '1?parents=1'))
+ ->assertStatus(200)
+ ->assertJsonFragment([
+ 'id' => 1
+ ]);
+
+ // check children
+ $d = json_decode_ex($response->baseResponse->getContent(), true);
+ $this->_testParentsValues($d, TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE, array_get($d, 'id'));
+ }
+
+ /**
+ * Getting value with parents, and match ids
+ */
+ /**
+ * @return void
+ */
+ public function testGetValueWithParents2()
+ {
+ $token = $this->getAdminAccessToken([ApiScope::VALUE_READ]);
+
+ $response = $this->withHeaders([
+ 'Authorization' => "Bearer $token",
+ ])->get(admin_urls('api', 'data', TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE_MANY_TO_MANY, '2?parents=1'))
+ ->assertStatus(200)
+ ->assertJsonFragment([
+ 'id' => 2
+ ]);
+
+ // check children
+ $d = json_decode_ex($response->baseResponse->getContent(), true);
+ $this->_testParentsValues($d, TestDefine::TESTDATA_TABLE_NAME_CHILD_TABLE_MANY_TO_MANY, array_get($d, 'id'));
+ }
+
/**
* @return void
*/
@@ -3038,4 +3134,55 @@ protected function _testChildrenValues($data, $table_name, $id)
}
}
}
+
+ /**
+ * @param mixed $data
+ * @param string $table_name
+ * @param mixed $id
+ * @return void
+ */
+ protected function _testParentsValues($data, $table_name, $id)
+ {
+ $relations = CustomRelation::getRelationsByChild($table_name);
+
+ // Whether has children
+ $this->assertTrue(array_has($data, 'parents'));
+ foreach ($relations as $relation) {
+ $this->assertTrue(array_has($data, 'parents.' . $relation->parent_custom_table_cache->table_name));
+ $parents = array_get($data, 'parents.' . $relation->parent_custom_table_cache->table_name);
+
+ if ($relation->relation_type == RelationType::ONE_TO_MANY) {
+ // Get value directly with parent_id
+ $value = $relation->child_custom_table_cache->getValueQuery()->find($id);
+ $parentId = array_get($parents, 'id');
+ $this->assertTrue($parentId == $value->parent_id, "parent_id expects equals {$value->parent_id}', but {$parentId}.");
+ }
+ ////// Check as n:n
+ else {
+ // get parents id
+ $parents_ids = collect($parents)->map(function ($parent) {
+ return array_get($parent, 'id');
+ })->toArray();
+
+ // Get value using pivot table
+ $parentIds = \DB::table($relation->getRelationName())
+ ->where('child_id', $id)
+ ->select('parent_id')
+ ->distinct()
+ ->pluck('parent_id')
+ ->toArray();
+
+ if (count($parentIds) == 0) {
+ $this->assertTrue(count($parents_ids) == 0, "{$id}' parents count expects 0, but real count is " . count($parents_ids));
+ } else {
+ foreach ($parentIds as $parentId) {
+ $this->assertTrue(in_array($parentId, $parents_ids), "{$parentId} expects containing {$id}' parents, but not has.");
+ }
+ foreach ($parents_ids as $parentId) {
+ $this->assertTrue(in_array($parentId, $parentIds), "{$parentId} expects containing {$id}' parents, but not has.");
+ }
+ }
+ }
+ }
+ }
}