diff --git a/app/src/Controller/ApiV2Controller.php b/app/src/Controller/ApiV2Controller.php index b44e1c640..11f3d280d 100644 --- a/app/src/Controller/ApiV2Controller.php +++ b/app/src/Controller/ApiV2Controller.php @@ -82,9 +82,7 @@ public function add() { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); - // $tableName = models - $tableName = $this->tableName; + $table = $this->getCurrentTable(); $json = $this->request->getData(); // Parsed by BodyParserMiddleware @@ -97,13 +95,13 @@ public function add() { foreach($json[$modelsName] as $rec) { try { - $obj = $this->$modelsName->newEntity($rec); + $obj = $table->newEntity($rec); - if($this->$modelsName->saveOrFail($obj)) { + if($table->saveOrFail($obj)) { $results[] = ['id' => $obj->id]; // Trigger provisioning, letting errors bubble up (AR-GMR-5) - if(method_exists($this->$modelsName, "requestProvisioning")) { + if(method_exists($table, "requestProvisioning")) { $this->llog('rule', "AR-GMR-5 Requesting provisioning for $modelsName " . $obj->id); $table->requestProvisioning(id: $obj->id, context: ProvisioningContextEnum::Automatic); } @@ -185,8 +183,8 @@ public function calculateRequestedCOID(): ?int { public function delete($id) { /** var string $modelsName */ $modelsName = $this->getName(); - /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + /** var Cake\ORM\Table $table */ + $table = $this->getCurrentTable(); // $tableName = models $tableName = $table->getTable(); @@ -237,7 +235,7 @@ protected function dispatchIndex(string $mode = 'default') { // $modelsName = Models $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); $reqParameters = [...$this->request->getQuery()]; $pickerMode = ($mode === 'picker'); @@ -271,7 +269,7 @@ public function edit($id) { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // $tableName = models $tableName = $table->getTable(); @@ -393,7 +391,7 @@ public function view($id = null) { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // $tableName = models $tableName = $table->getTable(); diff --git a/app/src/Controller/AppController.php b/app/src/Controller/AppController.php index 919ac0d22..60431d9dc 100644 --- a/app/src/Controller/AppController.php +++ b/app/src/Controller/AppController.php @@ -221,9 +221,6 @@ public function beforeFilter(\Cake\Event\EventInterface $event) { */ public function beforeRender(\Cake\Event\EventInterface $event) { - /** var string $modelsName */ - $modelsName = $this->getName(); - // Views can also inspect the request object to determine the current // controller and action, but it seems slightly easier to do it once here. $this->set('vv_controller', $this->request->getParam('controller')); @@ -269,6 +266,25 @@ public function getCOID(): ?int { return $cur_co ? $cur_co->id : null; } + /** + * Get the current Table instance for this controller, respecting plugin context. + * + * @since COmanage Registry v5.2.0 + * @return \Cake\ORM\Table + */ + protected function getCurrentTable(): \Cake\ORM\Table + { + /** @var string $modelsName */ + $modelsName = $this->getName(); + + $alias = $this->plugin !== null + ? $this->plugin . '.' . $modelsName + : $modelsName; + + return $this->fetchTable($alias); + } + + /** * @param string $potentialPrimaryLink * @@ -278,14 +294,11 @@ public function getCOID(): ?int { protected function primaryLinkOnGet(string $potentialPrimaryLink): Object|bool { - /** var string $modelsName */ - $modelsName = $this->getName(); - // If this action allows unkeyed, asserted primary link IDs, check the query // string (e.g.: 'add' or 'index' allow matchgrid_id to be passed in) $actionParam = $this->request->getParam('action'); - $allowsUnkeyed = $this->$modelsName->allowUnkeyedPrimaryLink($actionParam); - $allowsLookup = $this->$modelsName->allowLookupPrimaryLink($actionParam); + $allowsUnkeyed = $this->getCurrentTable()->allowUnkeyedPrimaryLink($actionParam); + $allowsLookup = $this->getCurrentTable()->allowLookupPrimaryLink($actionParam); $param = (int)$this->request->getParam('pass.0'); if($allowsUnkeyed) { @@ -299,7 +312,7 @@ protected function primaryLinkOnGet(string $potentialPrimaryLink): Object|bool return false; } - return $this->$modelsName->findPrimaryLink($param); + return $this->getCurrentTable()->findPrimaryLink($param); } /** @@ -345,20 +358,18 @@ protected function primaryLinkOnPost(string $potentialPrimaryLink): Object|bool protected function primaryLinkOnPut(): Object|bool { - /** var string $modelsName */ - $modelsName = $this->getName(); $param = (int)$this->request->getParam('pass.0'); // Put = edit, so we should look up the parent ID via the object itself if ( empty($param) || - !$this->$modelsName->allowLookupPrimaryLink($this->request->getParam('action')) + !$this->getCurrentTable()->allowLookupPrimaryLink($this->request->getParam('action')) ) { return false; } - return $this->$modelsName->findPrimaryLink($param); + return $this->getCurrentTable()->findPrimaryLink($param); } /** @@ -368,11 +379,9 @@ protected function primaryLinkOnPut(): Object|bool */ protected function populatedPrimaryLink(string $potentialPrimaryLink): Object { - /** var string $modelsName */ - $modelsName = $this->getName(); // $potentialPrimaryLink will be something like 'attribute_collector_id' // $potentialPrimaryLinkTable will be something like 'CoreEnroller.AttributeCollectors' - $potentialPrimaryLinkTable = $this->$modelsName->getPrimaryLinkTableName($potentialPrimaryLink); + $potentialPrimaryLinkTable = $this->getCurrentTable()->getPrimaryLinkTableName($potentialPrimaryLink); // For looking up values in records here, we want only the attribute // itself and not the plugin name (used for hacky notation by @@ -406,9 +415,8 @@ protected function populatedPrimaryLink(string $potentialPrimaryLink): Object protected function primaryLinkLookup(): void { - /** var string $modelsName */ - $modelsName = $this->getName(); - $availablePrimaryLinks = $this->$modelsName->getPrimaryLinks(); + $table = $this->getCurrentTable(); + $availablePrimaryLinks = $table->getPrimaryLinks(); // Iterate over all the potential primary links and pick the appropriate one foreach($availablePrimaryLinks as $potentialPrimaryLink) { @@ -429,7 +437,7 @@ protected function primaryLinkLookup(): void // At the end we need to have a Primary Link if(empty($this->cur_pl->value) - && !$this->$modelsName->allowEmptyPrimaryLink($this->request->getParam('action')) + && !$table->allowEmptyPrimaryLink($this->request->getParam('action')) && $this->request->getParam('action') != 'deleted') { throw new \RuntimeException(__d('error', 'primary_link')); } @@ -478,13 +486,10 @@ public function getPrimaryLink(bool $lookup=false): Object return $this->cur_pl; } - /** var string $modelsName */ - $modelsName = $this->getName(); - $this->cur_pl = new \stdClass(); - if(!(method_exists($this->$modelsName, 'getPrimaryLinks') - && $this->$modelsName->getPrimaryLinks()) + if(!(method_exists($this->getCurrentTable(), 'getPrimaryLinks') + && $this->getCurrentTable()->getPrimaryLinks()) ) { return $this->cur_pl; } @@ -504,7 +509,7 @@ public function getPrimaryLink(bool $lookup=false): Object // Look up the link value to find the related entity - $linkTableName = $this->$modelsName->getPrimaryLinkTableName($this->cur_pl->attr); + $linkTableName = $this->getCurrentTable()->getPrimaryLinkTableName($this->cur_pl->attr); $linkTable = $this->getTableLocator()->get($linkTableName); $this->set('vv_primary_link_model', $linkTableName); @@ -540,12 +545,9 @@ public function getPrimaryLink(bool $lookup=false): Object */ protected function getRedirectGoal(string $action): ?string { - /** var string $modelsName */ - $modelsName = $this->getName(); - // PrimaryLinkTrait - if(method_exists($this->$modelsName, "getRedirectGoal")) { - return $this->$modelsName->getRedirectGoal($this->request->getParam('action')); + if(method_exists($this->getCurrentTable(), "getRedirectGoal")) { + return $this->getCurrentTable()->getRedirectGoal($this->request->getParam('action')); } return 'index'; @@ -674,11 +676,10 @@ protected function setCO() { if($this->request->is('restful') && !empty($attrs['params']['model'])) { $modelsName = \Cake\Utility\Inflector::camelize($attrs['params']['model']); - $this->$modelsName = TableRegistry::getTableLocator()->get($modelsName); } - if(!method_exists($this->$modelsName, "requiresCO") - || !$this->$modelsName->requiresCO()) { + if(!method_exists($this->getCurrentTable(), "requiresCO") + || !$this->getCurrentTable()->requiresCO()) { // Nothing to do, CO not required by this model/controller return; } @@ -700,13 +701,13 @@ protected function setCO() { } if(!$coid - && $this->$modelsName->allowUnkeyedCO($this->request->getParam('action')) + && $this->getCurrentTable()->allowUnkeyedCO($this->request->getParam('action')) && !empty($this->request->getQuery('co_id'))) { $coid = $this->request->getQuery('co_id'); } if(!$coid - && !$this->$modelsName->allowEmptyCO() + && !$this->getCurrentTable()->allowEmptyCO() && !$this->request->is('restful')) { // If we get this far without a CO ID, something went wrong. throw new \RuntimeException(__d('error', 'coid')); @@ -728,7 +729,7 @@ protected function setCO() { throw new \InvalidArgumentException(__d('error', 'inactive', [__d('controller', 'Cos', [1]), $coid])); } - if(!empty($modelsName) && !empty($this->$modelsName)) { + if(!empty($modelsName) && !empty($this->getCurrentTable())) { // We store the CO ID in Configuration to facilitate its access from // model contexts such as validation where passing the value via the // Controller is not particularly feasible. Note that for API calls @@ -737,9 +738,9 @@ protected function setCO() { // This only works for the current model, not related models. For // relatedmodels, we use the event listener approach below. - if(method_exists($this->$modelsName, "acceptsCoId") - && $this->$modelsName->acceptsCoId()) { - $this->$modelsName->setCurCoId((int)$coid); + if(method_exists($this->getCurrentTable(), "acceptsCoId") + && $this->getCurrentTable()->acceptsCoId()) { + $this->getCurrentTable()->setCurCoId((int)$coid); } // This doesn't work for the current model since it has already been @@ -753,7 +754,7 @@ protected function setCO() { // a use case to do so, though note it's possible a child associations // wants the CO ID even though the parent doesn't. - foreach($this->$modelsName->associations()->getIterator() as $a) { + foreach($this->getCurrentTable()->associations()->getIterator() as $a) { $aTable = $a->getTarget(); if(method_exists($aTable, "acceptsCoId") @@ -773,9 +774,6 @@ protected function setCO() { */ protected function setTZ() { - /** var string $modelsName */ - $modelsName = $this->getName(); - // See if we've collected it from the browser in a previous page load. Otherwise, // use the system default. If the user set a preferred timezone, we'll catch that below. @@ -802,9 +800,9 @@ protected function setTZ() { $this->set('vv_tz', $tz); - if($this->$modelsName->behaviors()->has('Timezone')) { + if($this->getCurrentTable()->behaviors()->has('Timezone')) { // Tell TimezoneBehavior what the current timezone is - $this->$modelsName->setTimeZone($tz); + $this->getCurrentTable()->setTimeZone($tz); } } } \ No newline at end of file diff --git a/app/src/Controller/MVEAController.php b/app/src/Controller/MVEAController.php index 0af6cc082..6709e1ae8 100644 --- a/app/src/Controller/MVEAController.php +++ b/app/src/Controller/MVEAController.php @@ -47,6 +47,7 @@ class MVEAController extends StandardController { public function beforeFilter(\Cake\Event\EventInterface $event) { /** var string $modelsName */ $modelsName = $this->getName(); + $table = $this->getCurrentTable(); if(!$this->request->is('restful') && $this->request->getParam('action') != 'deleted') { // Provide additional hints to BreadcrumbsComponent. This needs to be here @@ -62,7 +63,7 @@ public function beforeFilter(\Cake\Event\EventInterface $event) { } else { $parentModel = StringUtilities::foreignKeyToClassName($primaryLink->attr); - $parentPrimaryLink = $this->$modelsName->$parentModel->findPrimaryLink((int)$primaryLink->value); + $parentPrimaryLink = $table->$parentModel->findPrimaryLink((int)$primaryLink->value); $this->Breadcrumb->injectPrimaryLink($parentPrimaryLink); $this->Breadcrumb->injectPrimaryLink($primaryLink); @@ -140,12 +141,13 @@ public function beforeFilter(\Cake\Event\EventInterface $event) { public function beforeRender(\Cake\Event\EventInterface $event) { /** var string $modelsName */ $modelsName = $this->getName(); + $table = $this->getCurrentTable(); // field = model (or model_name) $fieldName = Inflector::underscore(Inflector::singularize($modelsName)); if(!$this->request->is('restful') && $this->request->getParam('action') != 'deleted') { // If there is a default type setting for this model, pass it to the view - if($this->$modelsName->getSchema()->hasColumn('type_id')) { + if($table->getSchema()->hasColumn('type_id')) { $defaultTypeField = "default_" . $fieldName . "_type_id"; $CoSettings = TableRegistry::getTableLocator()->get('CoSettings'); diff --git a/app/src/Controller/MultipleAuthenticatorController.php b/app/src/Controller/MultipleAuthenticatorController.php index 5c9e32933..93570cbb7 100644 --- a/app/src/Controller/MultipleAuthenticatorController.php +++ b/app/src/Controller/MultipleAuthenticatorController.php @@ -52,7 +52,7 @@ public function beforeFilter(\Cake\Event\EventInterface $event) { // $authModelName = eg SshKeyAuthenticators $authModelsName = Inflector::singularize($modelsName) . "Authenticators"; /** var Cake\ORM\Table $table */ - $Table = $this->$modelsName; + $Table = $this->getCurrentTable(); // $authFK = eg ssh_key_authenticator_id $authFK = StringUtilities::classNameToForeignKey($authModelsName); @@ -102,7 +102,7 @@ public function generateRedirect($entity) { // $authModelName = eg SshKeyAuthenticators $authModelsName = Inflector::singularize($modelsName) . "Authenticators"; /** var Cake\ORM\Table $table */ - $Table = $this->$modelsName; + $Table = $this->getCurrentTable(); // $authFK = eg ssh_key_authenticator_id $authFK = StringUtilities::classNameToForeignKey($authModelsName); @@ -135,7 +135,7 @@ public function index() { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // $tableName = models $tableName = $table->getTable(); // Construct the Query @@ -185,7 +185,7 @@ public function willHandleAuth(\Cake\Event\EventInterface $event): string { // $authModelName = eg SshKeyAuthenticators $authModelsName = Inflector::singularize($modelsName) . "Authenticators"; /** var Cake\ORM\Table $table */ - $Table = $this->$modelsName; + $Table = $this->getCurrentTable(); // $authFK = eg ssh_key_authenticator_id $authFK = StringUtilities::classNameToForeignKey($authModelsName); diff --git a/app/src/Controller/SingleAuthenticatorController.php b/app/src/Controller/SingleAuthenticatorController.php index b2c9dd3f5..8634fc1ef 100644 --- a/app/src/Controller/SingleAuthenticatorController.php +++ b/app/src/Controller/SingleAuthenticatorController.php @@ -47,7 +47,7 @@ public function manage() { // $authModelName = eg PasswordAuthenticators $authModelsName = Inflector::singularize($modelsName) . "Authenticators"; /** var Cake\ORM\Table $table */ - $Table = $this->$modelsName; + $Table = $this->getCurrentTable(); // $authFK = eg password_authenticator_id $authFK = StringUtilities::classNameToForeignKey($authModelsName); diff --git a/app/src/Controller/StandardController.php b/app/src/Controller/StandardController.php index db341aff1..a6404d1bf 100644 --- a/app/src/Controller/StandardController.php +++ b/app/src/Controller/StandardController.php @@ -55,7 +55,7 @@ public function add() { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // $tableName = models $tableName = $table->getTable(); // Schema @@ -174,7 +174,7 @@ public function beforeRender(\Cake\Event\EventInterface $event) { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // Provide some hints to the views if($this->request->getParam('action') != 'deleted') { @@ -239,7 +239,7 @@ public function delete($id) { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // Allow a delete via a POST or DELETE $this->request->allowMethod(['post', 'delete']); @@ -376,17 +376,15 @@ public function edit(string $id) { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); - // $tableName = models - $tableName = $table->getTable(); + $table = $this->getCurrentTable(); // We use findById() rather than get() so we can apply subsequent // query modifications via traits $query = $table->findById($id); // QueryModificationTrait - if(method_exists($this->$modelsName, "getEditContains")) { - $query = $query->contain($this->$modelsName->getEditContains()); + if(method_exists($table, "getEditContains")) { + $query = $query->contain($table->getEditContains()); } try { @@ -589,7 +587,7 @@ protected function getFieldTypes() { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); $schema = $table->getSchema(); @@ -612,7 +610,7 @@ protected function getRequiredFields() { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // Build a list of required fields for FieldHelper $reqFields = []; @@ -639,7 +637,7 @@ public function index() { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // $tableName = models $tableName = $table->getTable(); // Construct the Query @@ -691,7 +689,7 @@ protected function populateAutoViewVars(object $obj=null) { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // AutoViewVarsTrait if(method_exists($table, 'getAutoViewVars') && $table->getAutoViewVars()) { @@ -712,7 +710,7 @@ public function provision($id) { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // $tableName = models $tableName = $table->getTable(); @@ -757,7 +755,7 @@ public function unfreeze($id) { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); try { // Pull the current record @@ -788,7 +786,7 @@ public function view($id = null) { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // $tableName = models $tableName = $table->getTable(); diff --git a/app/src/Controller/StandardEnrollerController.php b/app/src/Controller/StandardEnrollerController.php index 697a6db33..1335d3d38 100644 --- a/app/src/Controller/StandardEnrollerController.php +++ b/app/src/Controller/StandardEnrollerController.php @@ -104,7 +104,7 @@ public function calculatePermission(): bool { return false; } - $stepConfig = $this->$modelsName->get($modelId, ['contain' => ['EnrollmentFlowSteps' => ['EnrollmentFlows']]]); + $stepConfig = $this->getCurrentTable()->get($modelId, ['contain' => ['EnrollmentFlowSteps' => ['EnrollmentFlows']]]); $this->set('vv_step_config', $stepConfig); $this->set('vv_title', $stepConfig['enrollment_flow_step']['enrollment_flow']['name']); @@ -229,7 +229,7 @@ public function willHandleAuth(\Cake\Event\EventInterface $event): string { return 'notauth'; } - $stepConfig = $this->$modelsName->get($modelId, ['contain' => 'EnrollmentFlowSteps']); + $stepConfig = $this->getCurrentTable()->get($modelId, ['contain' => 'EnrollmentFlowSteps']); // Determine if the requested step is past the current/next step. // We don't allow steps that haven't run yet to be run out of order. diff --git a/app/src/Controller/StandardPluggableController.php b/app/src/Controller/StandardPluggableController.php index 5fdea66e8..fdee579da 100644 --- a/app/src/Controller/StandardPluggableController.php +++ b/app/src/Controller/StandardPluggableController.php @@ -49,7 +49,7 @@ public function configure(string $id) { /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); $parentId = $this->request->getParam('pass')[0]; $parentObj = $table->findById($parentId) diff --git a/app/src/Controller/StandardPluginController.php b/app/src/Controller/StandardPluginController.php index 66f8a1ee2..2d1d910fd 100644 --- a/app/src/Controller/StandardPluginController.php +++ b/app/src/Controller/StandardPluginController.php @@ -85,7 +85,7 @@ public function beforeRender(\Cake\Event\EventInterface $event) { /** var string $modelsName (ie: from ModelsTable, eg FileProvisionersTable) */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); $link = $this->getPrimaryLink(true); diff --git a/app/src/Lib/Traits/IndexQueryTrait.php b/app/src/Lib/Traits/IndexQueryTrait.php index 6e6ebb145..cb95b19fd 100644 --- a/app/src/Lib/Traits/IndexQueryTrait.php +++ b/app/src/Lib/Traits/IndexQueryTrait.php @@ -44,10 +44,8 @@ trait IndexQueryTrait { * @since COmanage Registry v5.0.0 */ public function constructGetIndexContains(Query $query): object { - /** var string $modelsName */ - $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // Initialize the containClause $containClause = []; @@ -73,10 +71,8 @@ public function constructGetIndexContains(Query $query): object { * @since COmanage Registry v5.0.0 */ public function constructGetPickerContains(Query $query): object { - /** var string $modelsName */ - $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // Initialize the containClause $containClause = []; @@ -104,10 +100,8 @@ public function constructGetPickerContains(Query $query): object { */ public function containClauseFromQueryParams(): array { - /** var string $modelsName */ - $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // Restfull and ajax do not include the IndexContains by default. $containClause = []; @@ -156,7 +150,7 @@ public function getIndexQuery(bool $pickerMode = false, array $requestParams = [ /** var string $modelsName */ $modelsName = $this->getName(); /** var Cake\ORM\Table $table */ - $table = $this->fetchTable($modelsName); + $table = $this->getCurrentTable(); // PrimaryLinkTrait $link = $this->getPrimaryLink(true); // Initialize the Query Object