diff --git a/src/xPDO/Om/mysql/xPDOManager.php b/src/xPDO/Om/mysql/xPDOManager.php index 26d2bdde..5a9e8b53 100644 --- a/src/xPDO/Om/mysql/xPDOManager.php +++ b/src/xPDO/Om/mysql/xPDOManager.php @@ -416,8 +416,18 @@ protected function getColumnDef($class, $name, $meta, array $options = array()) $notNull= !isset ($meta['null']) ? false : ($meta['null'] === 'false' || empty($meta['null'])); $null= $notNull ? ' NOT NULL' : ' NULL'; $extra= ''; - if (isset($meta['index']) && $meta['index'] == 'pk' && !is_array($pk) && $pktype == 'integer' && isset ($meta['generated']) && $meta['generated'] == 'native') { - $extra= ' AUTO_INCREMENT'; + $isGeneratedPkField = isset($meta['index']) && $meta['index'] == 'pk' + && isset($meta['generated']) && $meta['generated'] == 'native'; + if ($isGeneratedPkField && ($meta['phptype'] ?? '') === 'integer') { + if (!is_array($pk)) { + $extra = ' AUTO_INCREMENT'; + } else { + $primaryColumns = $this->xpdo->getIndexMeta($class)['PRIMARY']['columns'] ?? []; + reset($primaryColumns); + if (key($primaryColumns) === $name) { + $extra = ' AUTO_INCREMENT'; + } + } } if (empty ($extra) && isset ($meta['extra'])) { $extra= ' ' . $meta['extra']; diff --git a/src/xPDO/Om/xPDOObject.php b/src/xPDO/Om/xPDOObject.php index dcb4a641..5c549baf 100644 --- a/src/xPDO/Om/xPDOObject.php +++ b/src/xPDO/Om/xPDOObject.php @@ -1470,15 +1470,29 @@ public function save($cacheFlag= null) { if ($result) { if ($pkn && !$pk) { if ($pkGenerated) { - $this->_fields[$this->getPK()]= $this->getGeneratedKey(); + if (is_array($pkn)) { + $generatedKey = $this->getGeneratedKey(); + foreach ($pkn as $pkField => $v) { + if (isset($this->_fieldMeta[$pkField]['generated']) + && $this->_fieldMeta[$pkField]['generated'] === 'native') { + $this->_fields[$pkField] = $generatedKey; + break; + } + } + } else { + $this->_fields[$this->getPK()] = $this->getGeneratedKey(); + } } - $pk= $this->getPrimaryKey(); + $pk = $this->getPrimaryKey(); } if ($pk || !$this->getPK()) { $this->_dirty= array(); $this->_validated= array(); $this->_new= false; } + if (!$pk && $pkGenerated) { + $this->xpdo->log(xPDO::LOG_LEVEL_WARN, "Could not retrieve generated key for class {$this->_class}.", '', __METHOD__, __FILE__, __LINE__); + } $callback = $this->getOption(xPDO::OPT_CALLBACK_ON_SAVE); if ($callback && is_callable($callback)) { call_user_func($callback, array('className' => $this->_class, 'criteria' => $criteria, 'object' => $this)); diff --git a/test/model/schema/xPDO.Test.Sample.mysql.schema.xml b/test/model/schema/xPDO.Test.Sample.mysql.schema.xml index 99adf864..18b2a877 100644 --- a/test/model/schema/xPDO.Test.Sample.mysql.schema.xml +++ b/test/model/schema/xPDO.Test.Sample.mysql.schema.xml @@ -88,6 +88,16 @@ + + + + + + + + + + diff --git a/test/model/schema/xPDO.Test.Sample.pgsql.schema.xml b/test/model/schema/xPDO.Test.Sample.pgsql.schema.xml index fdc375e6..f5020c33 100644 --- a/test/model/schema/xPDO.Test.Sample.pgsql.schema.xml +++ b/test/model/schema/xPDO.Test.Sample.pgsql.schema.xml @@ -88,7 +88,6 @@ - diff --git a/test/model/xPDO/Test/Sample/NumberSeq.php b/test/model/xPDO/Test/Sample/NumberSeq.php new file mode 100644 index 00000000..a6552aae --- /dev/null +++ b/test/model/xPDO/Test/Sample/NumberSeq.php @@ -0,0 +1,14 @@ + '3.0', 'namespace' => 'xPDO\\Test\\Sample', 'namespacePrefix' => '', - 'class_map' => + 'class_map' => array ( - 'xPDO\\Om\\xPDOSimpleObject' => + 'xPDO\\Om\\xPDOSimpleObject' => array ( 0 => 'xPDO\\Test\\Sample\\Person', 1 => 'xPDO\\Test\\Sample\\Phone', @@ -13,12 +13,13 @@ 3 => 'xPDO\\Test\\Sample\\Item', 4 => 'xPDO\\Test\\Sample\\SecureObject', ), - 'xPDO\\Om\\xPDOObject' => + 'xPDO\\Om\\xPDOObject' => array ( 0 => 'xPDO\\Test\\Sample\\PersonPhone', 1 => 'xPDO\\Test\\Sample\\BloodType', + 2 => 'xPDO\\Test\\Sample\\NumberSeq', ), - 'xPDO\\Test\\Sample\\SecureObject' => + 'xPDO\\Test\\Sample\\SecureObject' => array ( 0 => 'xPDO\\Test\\Sample\\SecureItem', ), diff --git a/test/model/xPDO/Test/Sample/metadata.pgsql.php b/test/model/xPDO/Test/Sample/metadata.pgsql.php index 24433a71..2b197199 100644 --- a/test/model/xPDO/Test/Sample/metadata.pgsql.php +++ b/test/model/xPDO/Test/Sample/metadata.pgsql.php @@ -3,9 +3,9 @@ 'version' => '3.0', 'namespace' => 'xPDO\\Test\\Sample', 'namespacePrefix' => '', - 'class_map' => + 'class_map' => array ( - 'xPDO\\Om\\xPDOSimpleObject' => + 'xPDO\\Om\\xPDOSimpleObject' => array ( 0 => 'xPDO\\Test\\Sample\\Person', 1 => 'xPDO\\Test\\Sample\\Phone', @@ -13,12 +13,12 @@ 3 => 'xPDO\\Test\\Sample\\Item', 4 => 'xPDO\\Test\\Sample\\SecureObject', ), - 'xPDO\\Om\\xPDOObject' => + 'xPDO\\Om\\xPDOObject' => array ( 0 => 'xPDO\\Test\\Sample\\PersonPhone', 1 => 'xPDO\\Test\\Sample\\BloodType', ), - 'xPDO\\Test\\Sample\\SecureObject' => + 'xPDO\\Test\\Sample\\SecureObject' => array ( 0 => 'xPDO\\Test\\Sample\\SecureItem', ), diff --git a/test/model/xPDO/Test/Sample/metadata.sqlite.php b/test/model/xPDO/Test/Sample/metadata.sqlite.php index 24433a71..2b197199 100644 --- a/test/model/xPDO/Test/Sample/metadata.sqlite.php +++ b/test/model/xPDO/Test/Sample/metadata.sqlite.php @@ -3,9 +3,9 @@ 'version' => '3.0', 'namespace' => 'xPDO\\Test\\Sample', 'namespacePrefix' => '', - 'class_map' => + 'class_map' => array ( - 'xPDO\\Om\\xPDOSimpleObject' => + 'xPDO\\Om\\xPDOSimpleObject' => array ( 0 => 'xPDO\\Test\\Sample\\Person', 1 => 'xPDO\\Test\\Sample\\Phone', @@ -13,12 +13,12 @@ 3 => 'xPDO\\Test\\Sample\\Item', 4 => 'xPDO\\Test\\Sample\\SecureObject', ), - 'xPDO\\Om\\xPDOObject' => + 'xPDO\\Om\\xPDOObject' => array ( 0 => 'xPDO\\Test\\Sample\\PersonPhone', 1 => 'xPDO\\Test\\Sample\\BloodType', ), - 'xPDO\\Test\\Sample\\SecureObject' => + 'xPDO\\Test\\Sample\\SecureObject' => array ( 0 => 'xPDO\\Test\\Sample\\SecureItem', ), diff --git a/test/model/xPDO/Test/Sample/mysql/NumberSeq.php b/test/model/xPDO/Test/Sample/mysql/NumberSeq.php new file mode 100644 index 00000000..a67bd2c7 --- /dev/null +++ b/test/model/xPDO/Test/Sample/mysql/NumberSeq.php @@ -0,0 +1,65 @@ + 'xPDO\\Test\\Sample', + 'version' => '3.0', + 'table' => 'number_seq', + 'extends' => 'xPDO\\Om\\xPDOObject', + 'fields' => + array ( + 'level' => NULL, + 'number' => NULL, + ), + 'fieldMeta' => + array ( + 'level' => + array ( + 'dbtype' => 'varchar', + 'precision' => '1', + 'phptype' => 'string', + 'null' => false, + 'index' => 'pk', + ), + 'number' => + array ( + 'dbtype' => 'int', + 'precision' => '10', + 'attributes' => 'unsigned', + 'phptype' => 'integer', + 'null' => false, + 'index' => 'pk', + 'generated' => 'native', + ), + ), + 'indexes' => + array ( + 'PRIMARY' => + array ( + 'alias' => 'PRIMARY', + 'primary' => true, + 'unique' => true, + 'type' => 'BTREE', + 'columns' => + array ( + 'number' => + array ( + 'collation' => 'A', + 'null' => false, + ), + 'level' => + array ( + 'collation' => 'A', + 'null' => false, + ), + ), + ), + ), + ); + +} diff --git a/test/model/xPDO/Test/Sample/pgsql/NumberSeq.php b/test/model/xPDO/Test/Sample/pgsql/NumberSeq.php new file mode 100644 index 00000000..3a6574bb --- /dev/null +++ b/test/model/xPDO/Test/Sample/pgsql/NumberSeq.php @@ -0,0 +1,62 @@ + 'xPDO\\Test\\Sample', + 'version' => '3.0', + 'table' => 'number_seq', + 'extends' => 'xPDO\\Om\\xPDOObject', + 'fields' => + array ( + 'level' => NULL, + 'number' => NULL, + ), + 'fieldMeta' => + array ( + 'level' => + array ( + 'dbtype' => 'varchar', + 'precision' => '1', + 'phptype' => 'string', + 'null' => false, + 'index' => 'pk', + ), + 'number' => + array ( + 'dbtype' => 'integer', + 'phptype' => 'integer', + 'null' => false, + 'index' => 'pk', + ), + ), + 'indexes' => + array ( + 'PRIMARY' => + array ( + 'alias' => 'PRIMARY', + 'primary' => true, + 'unique' => true, + 'type' => 'BTREE', + 'columns' => + array ( + 'level' => + array ( + 'collation' => 'A', + 'null' => false, + ), + 'number' => + array ( + 'collation' => 'A', + 'null' => false, + ), + ), + ), + ), + ); + +} diff --git a/test/model/xPDO/Test/Sample/sqlite/NumberSeq.php b/test/model/xPDO/Test/Sample/sqlite/NumberSeq.php new file mode 100644 index 00000000..dabf7646 --- /dev/null +++ b/test/model/xPDO/Test/Sample/sqlite/NumberSeq.php @@ -0,0 +1,62 @@ + 'xPDO\\Test\\Sample', + 'version' => '3.0', + 'table' => 'number_seq', + 'extends' => 'xPDO\\Om\\xPDOObject', + 'fields' => + array ( + 'level' => NULL, + 'number' => NULL, + ), + 'fieldMeta' => + array ( + 'level' => + array ( + 'dbtype' => 'varchar', + 'precision' => '1', + 'phptype' => 'string', + 'null' => false, + 'index' => 'pk', + ), + 'number' => + array ( + 'dbtype' => 'int', + 'precision' => '10', + 'phptype' => 'integer', + 'null' => false, + 'index' => 'pk', + ), + ), + 'indexes' => + array ( + 'PRIMARY' => + array ( + 'alias' => 'PRIMARY', + 'primary' => true, + 'unique' => true, + 'columns' => + array ( + 'level' => + array ( + 'collation' => 'A', + 'null' => false, + ), + 'number' => + array ( + 'collation' => 'A', + 'null' => false, + ), + ), + ), + ), + ); + +} diff --git a/test/xPDO/Legacy/Om/xPDOObjectTest.php b/test/xPDO/Legacy/Om/xPDOObjectTest.php index 7d336fbc..cbcb0659 100644 --- a/test/xPDO/Legacy/Om/xPDOObjectTest.php +++ b/test/xPDO/Legacy/Om/xPDOObjectTest.php @@ -385,13 +385,14 @@ public function testGetObjectsByPK() */ public function testGetObjectGraphsByPK() { - //array method + $person = null; + $personPhone = null; + $phone = null; try { $person = $this->xpdo->getObjectGraph('Person', array('PersonPhone' => array('Phone' => array())), 2); if ($person) { $personPhoneColl = $person->getMany('PersonPhone'); if ($personPhoneColl) { - $phone = null; foreach ($personPhoneColl as $personPhone) { if ($personPhone->get('phone') == 2) { $phone = $personPhone->getOne('Phone'); @@ -404,7 +405,7 @@ public function testGetObjectGraphsByPK() $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $e->getMessage(), '', __METHOD__, __FILE__, __LINE__); } $this->assertTrue($person instanceof \Person, "Error retrieving Person object by primary key via getObjectGraph"); - $this->assertTrue($personPhone instanceof \PersonPhone, "Error retrieving retreiving related PersonPhone collection via getObjectGraph"); + $this->assertTrue($personPhone instanceof \PersonPhone, "Error retrieving related PersonPhone collection via getObjectGraph"); $this->assertTrue($phone instanceof \Phone, "Error retrieving related Phone object via getObjectGraph"); } @@ -413,13 +414,14 @@ public function testGetObjectGraphsByPK() */ public function testGetObjectGraphsJSONByPK() { - //JSON method + $person = null; + $personPhone = null; + $phone = null; try { $person = $this->xpdo->getObjectGraph('Person', '{"PersonPhone":{"Phone":{}}}', 2); if ($person) { $personPhoneColl = $person->getMany('PersonPhone'); if ($personPhoneColl) { - $phone = null; foreach ($personPhoneColl as $personPhone) { if ($personPhone->get('phone') == 2) { $phone = $personPhone->getOne('Phone'); @@ -432,7 +434,7 @@ public function testGetObjectGraphsJSONByPK() $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $e->getMessage(), '', __METHOD__, __FILE__, __LINE__); } $this->assertTrue($person instanceof \Person, "Error retrieving Person object by primary key via getObjectGraph, JSON graph"); - $this->assertTrue($personPhone instanceof \PersonPhone, "Error retrieving retreiving related PersonPhone collection via getObjectGraph, JSON graph"); + $this->assertTrue($personPhone instanceof \PersonPhone, "Error retrieving related PersonPhone collection via getObjectGraph, JSON graph"); $this->assertTrue($phone instanceof \Phone, "Error retrieving related Phone object via getObjectGraph, JSON graph"); } diff --git a/test/xPDO/Test/Om/xPDOObjectTest.php b/test/xPDO/Test/Om/xPDOObjectTest.php index 2117a2f3..77f9086e 100644 --- a/test/xPDO/Test/Om/xPDOObjectTest.php +++ b/test/xPDO/Test/Om/xPDOObjectTest.php @@ -257,6 +257,35 @@ public function providerGetCount() { ); } + /** + * Test saving an object with compound primary key that includes a generated field. + * Covers fix for #129: "PHP warning: Illegal offset type" when saving such objects. + * + * @requires extension pdo_mysql + */ + public function testSaveCompoundPkWithGeneratedField() + { + if (self::$properties['xpdo_driver'] !== 'mysql') { + $this->markTestSkipped('NumberSeq model is MySQL-only (compound PK with AUTO_INCREMENT)'); + } + $this->xpdo->getManager(); + $this->xpdo->manager->createObjectContainer('xPDO\\Test\\Sample\\NumberSeq'); + + $number = $this->xpdo->newObject('xPDO\\Test\\Sample\\NumberSeq'); + $number->fromArray(array('level' => 'B', 'number' => null), '', true); + + $result = $number->save(); + $msg = 'Save should succeed without PHP warnings'; + if (!$result) { + $msg .= '. Error: ' . print_r($this->xpdo->errorInfo(), true); + } + $this->assertTrue($result, $msg); + $this->assertNotNull($number->get('number'), 'Generated number field should be set after save'); + $this->assertFalse($number->isNew(), 'Object should not be new after save'); + + $this->xpdo->manager->removeObjectContainer('xPDO\\Test\\Sample\\NumberSeq'); + } + /** * Test saving an object. */ @@ -382,13 +411,14 @@ public function testGetObjectsByPK() */ public function testGetObjectGraphsByPK() { - //array method + $person = null; + $personPhone = null; + $phone = null; try { $person = $this->xpdo->getObjectGraph('xPDO\\Test\\Sample\\Person', array('PersonPhone' => array('Phone' => array())), 2); if ($person) { $personPhoneColl = $person->getMany('PersonPhone'); if ($personPhoneColl) { - $phone = null; foreach ($personPhoneColl as $personPhone) { if ($personPhone->get('phone') == 2) { $phone = $personPhone->getOne('Phone'); @@ -401,7 +431,7 @@ public function testGetObjectGraphsByPK() $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $e->getMessage(), '', __METHOD__, __FILE__, __LINE__); } $this->assertTrue($person instanceof \xPDO\Test\Sample\Person, "Error retrieving Person object by primary key via getObjectGraph"); - $this->assertTrue($personPhone instanceof \xPDO\Test\Sample\PersonPhone, "Error retrieving retreiving related PersonPhone collection via getObjectGraph"); + $this->assertTrue($personPhone instanceof \xPDO\Test\Sample\PersonPhone, "Error retrieving related PersonPhone collection via getObjectGraph"); $this->assertTrue($phone instanceof \xPDO\Test\Sample\Phone, "Error retrieving related Phone object via getObjectGraph"); } @@ -431,13 +461,14 @@ public function providerInvalidIntegerPKCriteria() */ public function testGetObjectGraphsJSONByPK() { - //JSON method + $person = null; + $personPhone = null; + $phone = null; try { $person = $this->xpdo->getObjectGraph('xPDO\\Test\\Sample\\Person', '{"PersonPhone":{"Phone":{}}}', 2); if ($person) { $personPhoneColl = $person->getMany('PersonPhone'); if ($personPhoneColl) { - $phone = null; foreach ($personPhoneColl as $personPhone) { if ($personPhone->get('phone') == 2) { $phone = $personPhone->getOne('Phone'); @@ -450,7 +481,7 @@ public function testGetObjectGraphsJSONByPK() $this->xpdo->log(xPDO::LOG_LEVEL_ERROR, $e->getMessage(), '', __METHOD__, __FILE__, __LINE__); } $this->assertTrue($person instanceof \xPDO\Test\Sample\Person, "Error retrieving Person object by primary key via getObjectGraph, JSON graph"); - $this->assertTrue($personPhone instanceof \xPDO\Test\Sample\PersonPhone, "Error retrieving retreiving related PersonPhone collection via getObjectGraph, JSON graph"); + $this->assertTrue($personPhone instanceof \xPDO\Test\Sample\PersonPhone, "Error retrieving related PersonPhone collection via getObjectGraph, JSON graph"); $this->assertTrue($phone instanceof \xPDO\Test\Sample\Phone, "Error retrieving related Phone object via getObjectGraph, JSON graph"); } diff --git a/test/xPDO/Test/xPDOTest.php b/test/xPDO/Test/xPDOTest.php index b285a601..e52ab636 100644 --- a/test/xPDO/Test/xPDOTest.php +++ b/test/xPDO/Test/xPDOTest.php @@ -247,6 +247,13 @@ public function providerGetAncestry() */ public function testGetDescendants($class, array $correct = array()) { + // NumberSeq only exists in the MySQL schema (compound PK with native-generated field + // is MySQL-only), so remove it from the expected set on other drivers. + if (self::$properties['xpdo_driver'] !== 'mysql') { + $correct = array_values(array_filter($correct, function ($c) { + return $c !== 'xPDO\\Test\\Sample\\NumberSeq'; + })); + } $this->assertEquals($correct, $this->xpdo->getDescendants($class)); } @@ -273,12 +280,13 @@ public function providerGetDescendants() 0 => 'xPDO\\Om\\xPDOSimpleObject', 1 => 'xPDO\\Test\\Sample\\PersonPhone', 2 => 'xPDO\\Test\\Sample\\BloodType', - 3 => 'xPDO\\Test\\Sample\\Person', - 4 => 'xPDO\\Test\\Sample\\Phone', - 5 => 'xPDO\\Test\\Sample\\xPDOSample', - 6 => 'xPDO\\Test\\Sample\\Item', - 7 => 'xPDO\\Test\\Sample\\SecureObject', - 8 => 'xPDO\\Test\\Sample\\SecureItem' + 3 => 'xPDO\\Test\\Sample\\NumberSeq', + 4 => 'xPDO\\Test\\Sample\\Person', + 5 => 'xPDO\\Test\\Sample\\Phone', + 6 => 'xPDO\\Test\\Sample\\xPDOSample', + 7 => 'xPDO\\Test\\Sample\\Item', + 8 => 'xPDO\\Test\\Sample\\SecureObject', + 9 => 'xPDO\\Test\\Sample\\SecureItem' ) ), );