├── ut
├── .gitignore
├── tpl.ut.yml
├── bootstrap.php
├── UTConfig.php
├── CasDemoUser.php
├── BasicGameInfo.php
├── ConsoleGame.php
├── Game.php
├── User.php
└── ItemManagerTest.php
├── .gitignore
├── bin
├── oasis-dynamodb-odm
└── oasis-dynamodb-odm.php
├── src
├── Exceptions
│ ├── NotAnnotatedException.php
│ ├── AnnotationParsingException.php
│ ├── UnderlyingDatabaseException.php
│ ├── ODMException.php
│ └── DataConsistencyException.php
├── Annotations
│ ├── PartitionedHashKey.php
│ ├── Field.php
│ ├── Item.php
│ └── Index.php
├── Console
│ ├── Commands
│ │ ├── DropSchemaCommand.php
│ │ ├── UpdateSchemaCommand.php
│ │ ├── CreateSchemaCommand.php
│ │ └── AbstractSchemaCommand.php
│ └── ConsoleHelper.php
├── DBAL
│ ├── Drivers
│ │ ├── AbstractDbConnection.php
│ │ ├── Connection.php
│ │ └── DynamoDbConnection.php
│ └── Schema
│ │ ├── AbstractSchemaTool.php
│ │ └── DynamoDbSchemaTool.php
├── ManagedItemState.php
├── ItemManager.php
├── ItemReflection.php
└── ItemRepository.php
├── phpunit.xml
├── odm-config.php
├── test.php
├── composer.json
├── LICENSE
└── README.md
/ut/.gitignore:
--------------------------------------------------------------------------------
1 | /ut.yml
2 | /cache/
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /.idea/
3 | /cache/
4 |
--------------------------------------------------------------------------------
/bin/oasis-dynamodb-odm:
--------------------------------------------------------------------------------
1 | oasis-dynamodb-odm.php
--------------------------------------------------------------------------------
/ut/tpl.ut.yml:
--------------------------------------------------------------------------------
1 | prefix: odm-ut-
2 | dynamodb:
3 | region: cn-north-1
4 | profile: beijing-minhao
5 |
--------------------------------------------------------------------------------
/ut/bootstrap.php:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 | ut/ItemManagerTest.php
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/ut/UTConfig.php:
--------------------------------------------------------------------------------
1 | addNamespace('Oasis\Mlib\ODM\Dynamodb\Ut', __DIR__ . "/ut");
22 |
23 | return new ConsoleHelper($im);
24 |
--------------------------------------------------------------------------------
/test.php:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env php
2 | id = 1;
23 | //$user->name = 'John';
24 | //$user->ver = '1';
25 | //$im->persist($user);
26 | //$im->flush();
27 |
28 | $user = $im->get(CasDemoUser::class, ['id' => 1]);
29 | $user->name = 'Alice';
30 | $user->ver = '2';
31 | sleep(5);
32 | $im->flush();
33 |
--------------------------------------------------------------------------------
/ut/CasDemoUser.php:
--------------------------------------------------------------------------------
1 | setName('odm:schema-tool:drop')
21 | ->setDescription('Drop the schema tables');
22 | }
23 |
24 | protected function execute(InputInterface $input, OutputInterface $output)
25 | {
26 | $schemaTool = $this->getItemManager()->createDBConnection()->getSchemaTool(
27 | $this->getItemManager(),
28 | $this->getManagedItemClasses(),
29 | [$output, "writeln"]
30 | );
31 |
32 | // create tables
33 | $schemaTool->dropSchema();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "oasis/dynamodb-odm",
3 | "description" : "ODM for dynamodb",
4 | "type" : "library",
5 | "require" : {
6 | "php" : ">=7.1",
7 | "doctrine/annotations": "^1.4",
8 | "oasis/aws-wrappers" : "^2.10",
9 | "oasis/logging" : "^1.0",
10 | "doctrine/common" : "^2.7",
11 | "symfony/console" : "^4.4",
12 | "symfony/finder" : "^4.4",
13 | "ext-json": "*"
14 | },
15 | "require-dev" : {
16 | "phpunit/phpunit": "^5.7"
17 | },
18 | "license" : "MIT",
19 | "authors" : [
20 | {
21 | "name" : "Minhao Zhang",
22 | "email": "minhao.zhang@qq.com"
23 | }
24 | ],
25 | "autoload" : {
26 | "psr-4": {
27 | "Oasis\\Mlib\\ODM\\Dynamodb\\": "src/"
28 | }
29 | },
30 | "autoload-dev": {
31 | "psr-4": {
32 | "Oasis\\Mlib\\ODM\\Dynamodb\\Ut\\": "ut/"
33 | }
34 | },
35 | "bin" : [
36 | "bin/oasis-dynamodb-odm"
37 | ]
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Minhao Zhang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Console/ConsoleHelper.php:
--------------------------------------------------------------------------------
1 | itemManager = $itemManager;
27 | }
28 |
29 | public function addCommands(Application $application)
30 | {
31 | $application->addCommands(
32 | [
33 | (new CreateSchemaCommand())->withItemManager($this->itemManager),
34 | (new DropSchemaCommand())->withItemManager($this->itemManager),
35 | (new UpdateSchemaCommand())->withItemManager($this->itemManager),
36 | ]
37 | );
38 | }
39 |
40 | /**
41 | * @return ItemManager
42 | */
43 | public function getItemManager()
44 | {
45 | return $this->itemManager;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Console/Commands/UpdateSchemaCommand.php:
--------------------------------------------------------------------------------
1 | setName('odm:schema-tool:update')
23 | ->setDescription('Update the schema tables')
24 | ->addOption(
25 | 'dry-run',
26 | 'd',
27 | InputOption::VALUE_NONE,
28 | "dry run: prints out changes without really updating schema"
29 | );
30 | }
31 |
32 | protected function execute(InputInterface $input, OutputInterface $output)
33 | {
34 | $isDryRun = $input->getOption('dry-run');
35 | $schemaTool = $this->getItemManager()->createDBConnection()->getSchemaTool(
36 | $this->getItemManager(),
37 | $this->getManagedItemClasses(),
38 | [$output, "writeln"]
39 | );
40 |
41 | // create tables
42 | $schemaTool->updateSchema($isDryRun);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/ut/BasicGameInfo.php:
--------------------------------------------------------------------------------
1 | family;
43 | }
44 |
45 | /**
46 | * @param string $family
47 | */
48 | public function setFamily($family)
49 | {
50 | $this->family = $family;
51 | }
52 |
53 | /**
54 | * @return string
55 | */
56 | public function getGamecode()
57 | {
58 | return $this->gamecode;
59 | }
60 |
61 | /**
62 | * @param string $gamecode
63 | */
64 | public function setGamecode($gamecode)
65 | {
66 | $this->gamecode = $gamecode;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/DBAL/Drivers/AbstractDbConnection.php:
--------------------------------------------------------------------------------
1 | dbConfig = $dbConfig;
41 | }
42 |
43 | public function getDatabaseConfig()
44 | {
45 | return $this->dbConfig;
46 | }
47 |
48 | public function setTableName($tableName)
49 | {
50 | $this->tableName = $tableName;
51 | }
52 |
53 | public function setAttributeTypes($attributeTypes)
54 | {
55 | $this->attributeTypes = $attributeTypes;
56 | }
57 |
58 | public function setItemReflection(ItemReflection $itemReflection)
59 | {
60 | $this->itemReflection = $itemReflection;
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/src/Annotations/Field.php:
--------------------------------------------------------------------------------
1 | setName('odm:schema-tool:create')
23 | ->setDescription('Processes the schema and create corresponding tables and indices.')
24 | ->addOption('skip-existing-table', null, InputOption::VALUE_NONE, "skip creating existing table!")
25 | ->addOption(
26 | 'dry-run',
27 | null,
28 | InputOption::VALUE_NONE,
29 | "output possible table creations without actually creating them."
30 | );
31 | }
32 |
33 | protected function execute(InputInterface $input, OutputInterface $output)
34 | {
35 | $skipExisting = $input->getOption('skip-existing-table');
36 | $dryRun = $input->getOption('dry-run');
37 | $schemaTool = $this->getItemManager()->createDBConnection()->getSchemaTool(
38 | $this->getItemManager(),
39 | $this->getManagedItemClasses(),
40 | [$output, "writeln"]
41 | );
42 |
43 | // create tables
44 | $schemaTool->createSchema($skipExisting, $dryRun);
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/ut/ConsoleGame.php:
--------------------------------------------------------------------------------
1 | achievements;
52 | }
53 |
54 | /**
55 | * @param array $achievements
56 | */
57 | public function setAchievements($achievements)
58 | {
59 | $this->achievements = $achievements;
60 | }
61 |
62 | /**
63 | * @return array
64 | */
65 | public function getAuthors()
66 | {
67 | return $this->authors;
68 | }
69 |
70 | /**
71 | * @param array $authors
72 | */
73 | public function setAuthors($authors)
74 | {
75 | $this->authors = $authors;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/DBAL/Schema/AbstractSchemaTool.php:
--------------------------------------------------------------------------------
1 | itemManager = $im;
37 | $this->classReflections = $classReflections;
38 | $this->outputFunction = $outputFunction;
39 | }
40 |
41 | abstract public function createSchema($skipExisting, $dryRun);
42 |
43 | abstract public function updateSchema($isDryRun);
44 |
45 | abstract public function dropSchema();
46 |
47 | /**
48 | * @return ItemManager
49 | */
50 | protected function getItemManager()
51 | {
52 | return $this->itemManager;
53 | }
54 |
55 | protected function getManagedItemClasses()
56 | {
57 | return $this->classReflections;
58 | }
59 |
60 | protected function outputWrite($message)
61 | {
62 | if (is_callable($this->outputFunction)) {
63 | $output = $this->outputFunction;
64 | $output($message);
65 | }
66 | else {
67 | mnotice($message);
68 | }
69 | }
70 |
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/src/Console/Commands/AbstractSchemaCommand.php:
--------------------------------------------------------------------------------
1 | itemManager = $itemManager;
30 |
31 | return $this;
32 | }
33 |
34 | /**
35 | * @return ItemManager
36 | */
37 | public function getItemManager()
38 | {
39 | return $this->itemManager;
40 | }
41 |
42 | /**
43 | * @noinspection PhpRedundantCatchClauseInspection
44 | */
45 | protected function getManagedItemClasses()
46 | {
47 | $classes = [];
48 | foreach ($this->itemManager->getPossibleItemClasses() as $class) {
49 | try {
50 | $reflection = $this->itemManager->getItemReflection($class);
51 | } catch (NotAnnotatedException $e) {
52 | continue;
53 | } catch (ReflectionException $e) {
54 | continue;
55 | } catch (Exception $e) {
56 | mtrace($e, "Annotation parsing exception found: ", 'error');
57 | throw $e;
58 | }
59 | $classes[$class] = $reflection;
60 | }
61 |
62 | return $classes;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/bin/oasis-dynamodb-odm.php:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env php
2 | setCatchExceptions(true);
68 | $consoleHelper->addCommands($cli);
69 | $cli->run();
70 |
--------------------------------------------------------------------------------
/src/Annotations/Item.php:
--------------------------------------------------------------------------------
1 | $value) {
56 | if (property_exists(self::class, $name)) {
57 |
58 | switch ($name) {
59 | case 'primaryIndex':
60 | if (!$value instanceof Index) {
61 | $value = new Index($value);
62 | }
63 | break;
64 | case 'globalSecondaryIndices':
65 | case 'localSecondaryIndices':
66 | $orig = $value;
67 | $value = [];
68 | foreach ($orig as $indexValue) {
69 | if (!$indexValue instanceof Index) {
70 | $indexValue = new Index($indexValue);
71 | }
72 | $value[] = $indexValue;
73 | }
74 | break;
75 | }
76 |
77 | $this->$name = $value;
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/ut/Game.php:
--------------------------------------------------------------------------------
1 | family;
63 | }
64 |
65 | /**
66 | * @param string $family
67 | */
68 | public function setFamily($family)
69 | {
70 | $this->family = $family;
71 | }
72 |
73 | /**
74 | * @return string
75 | */
76 | public function getGamecode()
77 | {
78 | return $this->gamecode;
79 | }
80 |
81 | /**
82 | * @param string $gamecode
83 | */
84 | public function setGamecode($gamecode)
85 | {
86 | $this->gamecode = $gamecode;
87 | }
88 |
89 | /**
90 | * @return string
91 | */
92 | public function getLanguage()
93 | {
94 | return $this->language;
95 | }
96 |
97 | /**
98 | * @param string $language
99 | */
100 | public function setLanguage($language)
101 | {
102 | $this->language = $language;
103 | }
104 |
105 | /**
106 | * @return int
107 | */
108 | public function getLastUpdatedAt()
109 | {
110 | return $this->lastUpdatedAt;
111 | }
112 |
113 | /**
114 | * @param int $lastUpdatedAt
115 | */
116 | public function setLastUpdatedAt($lastUpdatedAt)
117 | {
118 | $this->lastUpdatedAt = $lastUpdatedAt;
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/Annotations/Index.php:
--------------------------------------------------------------------------------
1 | hash = $values[0];
43 | if (isset($values[1])) {
44 | $this->range = $values[1];
45 | }
46 | if (isset($values[2])) {
47 | $this->name = $values[2];
48 | }
49 | }
50 | elseif (isset($values['hash'])) {
51 | $this->hash = $values['hash'];
52 | if (isset($values['range'])) {
53 | $this->range = $values['range'];
54 | }
55 | if (isset($values['name'])) {
56 | $this->name = $values['name'];
57 | }
58 | }
59 | else {
60 | throw new AnnotationParsingException("Index must be constructed with an array of hash and range keys");
61 | }
62 | }
63 |
64 | public function getKeys()
65 | {
66 | $ret = [
67 | $this->hash,
68 | ];
69 | if ($this->range) {
70 | $ret[] = $this->range;
71 | }
72 |
73 | return $ret;
74 | }
75 |
76 | public function getDynamodbIndex(array $fieldNameMapping, array $attributeTypes)
77 | {
78 | $hash = $fieldNameMapping[$this->hash];
79 | $range = $this->range ? $fieldNameMapping[$this->range] : '';
80 |
81 | if (!isset($attributeTypes[$hash])
82 | || ($range && !isset($attributeTypes[$range]))
83 | ) {
84 | throw new ODMException("Index key is not defined as Field!");
85 | }
86 |
87 | $hashType = $attributeTypes[$hash];
88 | $rangeKey = $range ? : null;
89 | $rangeType = $range ? $attributeTypes[$range] : 'string';
90 | $hashType = constant(DynamoDbItem::class . '::ATTRIBUTE_TYPE_' . strtoupper($hashType));
91 | $rangeType = constant(DynamoDbItem::class . '::ATTRIBUTE_TYPE_' . strtoupper($rangeType));
92 | $idx = new DynamoDbIndex($hash, $hashType, $rangeKey, $rangeType);
93 | if ($this->name) {
94 | $idx->setName($this->name);
95 | }
96 |
97 | return $idx;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/ut/User.php:
--------------------------------------------------------------------------------
1 | age;
87 | }
88 |
89 | /**
90 | * @param mixed $age
91 | */
92 | public function setAge($age)
93 | {
94 | $this->age = $age;
95 | }
96 |
97 | /**
98 | * @return mixed
99 | */
100 | public function getAlias()
101 | {
102 | return $this->alias;
103 | }
104 |
105 | /**
106 | * @param mixed $alias
107 | */
108 | public function setAlias($alias)
109 | {
110 | $this->alias = $alias;
111 | }
112 |
113 | /**
114 | * @return mixed
115 | */
116 | public function getHometown()
117 | {
118 | return $this->hometown;
119 | }
120 |
121 | /**
122 | * @param mixed $hometown
123 | */
124 | public function setHometown($hometown)
125 | {
126 | $this->hometown = $hometown;
127 | }
128 |
129 | /**
130 | * @return int
131 | */
132 | public function getId()
133 | {
134 | return $this->id;
135 | }
136 |
137 | /**
138 | * @param int $id
139 | */
140 | public function setId($id)
141 | {
142 | $this->id = $id;
143 | }
144 |
145 | /**
146 | * @return mixed
147 | */
148 | public function getLastUpdated()
149 | {
150 | return $this->lastUpdated;
151 | }
152 |
153 | /**
154 | * @param mixed $lastUpdated
155 | */
156 | public function setLastUpdated($lastUpdated)
157 | {
158 | $this->lastUpdated = $lastUpdated;
159 | }
160 |
161 | /**
162 | * @return mixed
163 | */
164 | public function getName()
165 | {
166 | return $this->name;
167 | }
168 |
169 | /**
170 | * @param mixed $name
171 | */
172 | public function setName($name)
173 | {
174 | $this->name = $name;
175 | }
176 |
177 | /**
178 | * @return mixed
179 | */
180 | public function getWage()
181 | {
182 | return $this->wage;
183 | }
184 |
185 | /**
186 | * @param mixed $wage
187 | */
188 | public function setWage($wage)
189 | {
190 | $this->wage = $wage;
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/DBAL/Drivers/Connection.php:
--------------------------------------------------------------------------------
1 | itemReflection = $itemReflection;
36 | $this->item = $item;
37 | $this->originalData = $originalData;
38 | }
39 |
40 | public function hasDirtyData()
41 | {
42 | if ($this->state != self::STATE_MANAGED) {
43 | return false;
44 | }
45 |
46 | $data = $this->itemReflection->dehydrate($this->item);
47 | if (!$this->isDataEqual($data, $this->originalData)) {
48 | return true;
49 | }
50 | else {
51 | return false;
52 | }
53 | }
54 |
55 | /**
56 | * @return bool
57 | */
58 | public function isNew()
59 | {
60 | return $this->state == self::STATE_NEW;
61 | }
62 |
63 | /**
64 | * @return bool
65 | */
66 | public function isRemoved()
67 | {
68 | return $this->state == self::STATE_REMOVED;
69 | }
70 |
71 | public function updatePartitionedHashKeys($hashFunction = null)
72 | {
73 | foreach ($this->itemReflection->getPartitionedHashKeys() as $partitionedHashKey => $def) {
74 | $baseValue = $this->itemReflection->getPropertyValue($this->item, $def->baseField);
75 | $hashSource = $this->itemReflection->getPropertyValue($this->item, $def->hashField);
76 | if (is_callable($hashFunction)) {
77 | $hashSource = call_user_func($hashFunction, $hashSource);
78 | }
79 | $hashNumber = hexdec(substr(md5($hashSource), 0, 8));
80 | $hashRemainder = dechex($hashNumber % $def->size);
81 | $hashResult = sprintf("%s-%s", $baseValue, $hashRemainder);
82 | $this->itemReflection->updateProperty($this->item, $partitionedHashKey, $hashResult);
83 | }
84 | }
85 |
86 | public function updateCASTimestamps($timestampOffset = 0)
87 | {
88 | $now = time() + $timestampOffset;
89 | foreach ($this->itemReflection->getCasProperties() as $propertyName => $casType) {
90 | if ($casType == Field::CAS_TIMESTAMP) {
91 | $this->itemReflection->updateProperty($this->item, $propertyName, $now);
92 | }
93 | }
94 | }
95 |
96 | /**
97 | * @return array
98 | */
99 | public function getCheckConditionData()
100 | {
101 | $checkValues = [];
102 | foreach ($this->itemReflection->getCasProperties() as $propertyName => $casType) {
103 | $fieldName = $this->itemReflection->getFieldNameByPropertyName($propertyName);
104 | $checkValues[$fieldName] = isset($this->originalData[$fieldName]) ? $this->originalData[$fieldName] : null;
105 | }
106 |
107 | return $checkValues;
108 | }
109 |
110 | /**
111 | * @return mixed
112 | */
113 | public function getItem()
114 | {
115 | return $this->item;
116 | }
117 |
118 | /**
119 | * @param mixed $item
120 | */
121 | public function setItem($item)
122 | {
123 | $this->item = $item;
124 | }
125 |
126 | /**
127 | * @return array
128 | */
129 | public function getOriginalData()
130 | {
131 | return $this->originalData;
132 | }
133 |
134 | /**
135 | * @param array $originalData
136 | */
137 | public function setOriginalData($originalData)
138 | {
139 | $this->originalData = $originalData;
140 | }
141 |
142 | public function getOriginalValue($key)
143 | {
144 | if (isset($this->originalData[$key])) {
145 | return $this->originalData[$key];
146 | }
147 | else {
148 | return null;
149 | }
150 | }
151 |
152 | /**
153 | * @param int $state
154 | */
155 | public function setState($state)
156 | {
157 | $this->state = $state;
158 | }
159 |
160 | public function setUpdated()
161 | {
162 | $this->originalData = $this->itemReflection->dehydrate($this->item);
163 | }
164 |
165 | /** @noinspection PhpParamsInspection */
166 | protected function isDataEqual(&$a, &$b)
167 | {
168 | // empty string is considered null in dynamodb
169 | if (
170 | (is_null($a) && is_string($b) && $b === '')
171 | || (is_null($b) && is_string($a) && $a === '')
172 | ) {
173 | return true;
174 | }
175 |
176 | if (gettype($a) != gettype($b)) {
177 | return false;
178 | }
179 |
180 | switch (true) {
181 | case (is_double($a)):
182 | return "$a" == "$b";
183 | break;
184 | case (is_array($a)):
185 | if (count($a) !== count($b)) {
186 | return false;
187 | }
188 | foreach ($a as $k => &$v) {
189 | if (!key_exists($k, $b)) {
190 | return false;
191 | }
192 | if (!$this->isDataEqual($v, $b[$k])) {
193 | return false;
194 | }
195 | }
196 |
197 | // every $k in $a can be found in $b and is equal
198 | return true;
199 | break;
200 | case (is_resource($a)):
201 | case (is_object($a)):
202 | throw new ODMException("DynamoDb data cannot contain value of resource/object");
203 | break;
204 | default:
205 | return $a === $b;
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/ItemManager.php:
--------------------------------------------------------------------------------
1 | dbConnection = $dbCnn;
78 | }
79 | else {
80 | $this->databaseConfig = $dbCnn;
81 | }
82 |
83 | $this->defaultTablePrefix = $defaultTablePrefix;
84 |
85 | AnnotationRegistry::registerLoader([$this, 'loadAnnotationClass']);
86 |
87 | $this->reader = new CachedReader(
88 | new AnnotationReader(),
89 | new FilesystemCache($cacheDir),
90 | $isDev
91 | );
92 | }
93 |
94 | public function addNamespace($namespace, $srcDir)
95 | {
96 | if (!is_dir($srcDir)) {
97 | mwarning("Directory %s doesn't exist.", $srcDir);
98 |
99 | return;
100 | }
101 | $finder = new Finder();
102 | $finder->in($srcDir)
103 | ->path('/\.php$/');
104 | foreach ($finder as $splFileInfo) {
105 | $classname = sprintf(
106 | "%s\\%s\\%s",
107 | $namespace,
108 | str_replace("/", "\\", $splFileInfo->getRelativePath()),
109 | $splFileInfo->getBasename(".php")
110 | );
111 | $classname = preg_replace('#\\\\+#', '\\', $classname);
112 | //mdebug("Class name is %s", $classname);
113 | $this->possibleItemClasses[] = $classname;
114 | }
115 | }
116 |
117 | public function addReservedAttributeNames(...$args)
118 | {
119 | foreach ($args as $arg) {
120 | $this->reservedAttributeNames[] = $arg;
121 | }
122 | }
123 |
124 | public function clear()
125 | {
126 | foreach ($this->repositories as $itemRepository) {
127 | $itemRepository->clear();
128 | }
129 | }
130 |
131 | public function detach($item)
132 | {
133 | if (!is_object($item)) {
134 | throw new ODMException("You can only detach a managed object!");
135 | }
136 | $this->getRepository(get_class($item))->detach($item);
137 | }
138 |
139 | public function flush()
140 | {
141 | foreach ($this->repositories as $repository) {
142 | $repository->flush();
143 | }
144 | }
145 |
146 | public function get($itemClass, array $keys, $consistentRead = false)
147 | {
148 | return $this->getRepository($itemClass)->get($keys, $consistentRead);
149 | }
150 |
151 | /**
152 | * @deprecated use shouldSkipCheckAndSet() instead
153 | * @return bool
154 | */
155 | public function isSkipCheckAndSet()
156 | {
157 | return $this->skipCheckAndSet;
158 | }
159 |
160 | /**
161 | * @param bool $skipCheckAndSet
162 | */
163 | public function setSkipCheckAndSet($skipCheckAndSet)
164 | {
165 | $this->skipCheckAndSet = $skipCheckAndSet;
166 | }
167 |
168 | /**
169 | * @param $className
170 | *
171 | * @internal
172 | * @return bool
173 | */
174 | public function loadAnnotationClass($className)
175 | {
176 | if (class_exists($className)) {
177 | return true;
178 | }
179 | else {
180 | return false;
181 | }
182 | }
183 |
184 | public function persist($item)
185 | {
186 | $this->getRepository(get_class($item))->persist($item);
187 | }
188 |
189 | public function refresh($item, $persistIfNotManaged = false)
190 | {
191 | $this->getRepository(get_class($item))->refresh($item, $persistIfNotManaged);
192 | }
193 |
194 | public function remove($item)
195 | {
196 | $this->getRepository(get_class($item))->remove($item);
197 | }
198 |
199 | /**
200 | * @return bool
201 | */
202 | public function shouldSkipCheckAndSet()
203 | {
204 | return $this->skipCheckAndSet;
205 | }
206 |
207 | /**
208 | * @return mixed
209 | */
210 | public function getDefaultTablePrefix()
211 | {
212 | return $this->defaultTablePrefix;
213 | }
214 |
215 | /**
216 | * @return array
217 | * todo: set deprecated and recommend getDatabaseConfig() instead
218 | */
219 | public function getDynamodbConfig()
220 | {
221 | return $this->getDatabaseConfig();
222 | }
223 |
224 | /**
225 | * @return array
226 | */
227 | public function getDatabaseConfig()
228 | {
229 | if ($this->dbConnection instanceof Connection) {
230 | return $this->dbConnection->getDatabaseConfig();
231 | }
232 |
233 | return $this->databaseConfig;
234 | }
235 |
236 | /**
237 | * @param $itemClass
238 | *
239 | * @return ItemReflection
240 | */
241 | public function getItemReflection($itemClass)
242 | {
243 | if (!isset($this->itemReflections[$itemClass])) {
244 | $reflection = new ItemReflection($itemClass, $this->reservedAttributeNames);
245 | $reflection->parse($this->reader);
246 | $this->itemReflections[$itemClass] = $reflection;
247 | }
248 | else {
249 | $reflection = $this->itemReflections[$itemClass];
250 | }
251 |
252 | return $reflection;
253 | }
254 |
255 | /**
256 | * @return string[]
257 | */
258 | public function getPossibleItemClasses()
259 | {
260 | return $this->possibleItemClasses;
261 | }
262 |
263 | /**
264 | * @return AnnotationReader
265 | */
266 | public function getReader()
267 | {
268 | return $this->reader;
269 | }
270 |
271 | /**
272 | * @param $itemClass
273 | *
274 | * @return ItemRepository
275 | */
276 | public function getRepository($itemClass)
277 | {
278 | if (!isset($this->repositories[$itemClass])) {
279 | $reflection = $this->getItemReflection($itemClass);
280 | $repoClass = $reflection->getRepositoryClass();
281 | $repo = new $repoClass(
282 | $reflection,
283 | $this
284 | );
285 | $this->repositories[$itemClass] = $repo;
286 | }
287 | else {
288 | $repo = $this->repositories[$itemClass];
289 | }
290 |
291 | return $repo;
292 | }
293 |
294 | /**
295 | * @return array
296 | */
297 | public function getReservedAttributeNames()
298 | {
299 | return $this->reservedAttributeNames;
300 | }
301 |
302 | /**
303 | * @param array $reservedAttributeNames
304 | */
305 | public function setReservedAttributeNames($reservedAttributeNames)
306 | {
307 | $this->reservedAttributeNames = $reservedAttributeNames;
308 | }
309 |
310 | /**
311 | *
312 | * Get a new database connection
313 | *
314 | * @return Connection
315 | */
316 | public function createDBConnection()
317 | {
318 | if ($this->dbConnection instanceof Connection) {
319 | return clone $this->dbConnection;
320 | }
321 | else {
322 | // default database connection
323 | return new DynamoDbConnection($this->getDatabaseConfig());
324 | }
325 | }
326 |
327 | }
328 |
--------------------------------------------------------------------------------
/src/DBAL/Drivers/DynamoDbConnection.php:
--------------------------------------------------------------------------------
1 | dynamodbTable !== null) {
35 | return $this->dynamodbTable;
36 | }
37 |
38 | if (empty($this->tableName)) {
39 | throw new ODMException("Unknown table name to initialize DynamoDbTable client");
40 | }
41 |
42 | if (empty($this->attributeTypes)) {
43 | throw new ODMException("Unknown attribute types to initialize DynamoDbTable client");
44 | }
45 |
46 | $this->dynamodbTable = new DynamoDbTable(
47 | $this->dbConfig,
48 | $this->tableName,
49 | $this->attributeTypes
50 | );
51 |
52 | return $this->dynamodbTable;
53 | }
54 |
55 | public function batchGet(
56 | array $keys,
57 | $isConsistentRead = false,
58 | $concurrency = 10,
59 | $projectedFields = [],
60 | $keyIsTyped = false,
61 | $retryDelay = 0,
62 | $maxDelay = 15000
63 | ) {
64 | return $this->getDynamodbTable()->batchGet(
65 | $keys,
66 | $isConsistentRead,
67 | $concurrency,
68 | $projectedFields,
69 | $keyIsTyped,
70 | $retryDelay,
71 | $maxDelay
72 | );
73 | }
74 |
75 | public function batchDelete(array $objs, $concurrency = 10, $maxDelay = 15000)
76 | {
77 | $this->getDynamodbTable()->batchDelete($objs, $concurrency, $maxDelay);
78 | }
79 |
80 | public function batchPut(array $objs, $concurrency = 10, $maxDelay = 15000)
81 | {
82 | $this->getDynamodbTable()->batchPut($objs, $concurrency, $maxDelay);
83 | }
84 |
85 | public function set(array $obj, $checkValues = [])
86 | {
87 | return $this->getDynamodbTable()->set($obj, $checkValues);
88 | }
89 |
90 | public function get(array $keys, $is_consistent_read = false, $projectedFields = [])
91 | {
92 | return $this->getDynamodbTable()->get($keys, $is_consistent_read, $projectedFields);
93 | }
94 |
95 | public function query(
96 | $keyConditions,
97 | array $fieldsMapping,
98 | array $paramsMapping,
99 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
100 | $filterExpression = '',
101 | &$lastKey = null,
102 | $evaluationLimit = 30,
103 | $isConsistentRead = false,
104 | $isAscendingOrder = true,
105 | $projectedFields = []
106 | ) {
107 | return $this->getDynamodbTable()->query(
108 | $keyConditions,
109 | $fieldsMapping,
110 | $paramsMapping,
111 | $indexName,
112 | $filterExpression,
113 | $lastKey,
114 | $evaluationLimit,
115 | $isConsistentRead,
116 | $isAscendingOrder,
117 | $projectedFields
118 | );
119 | }
120 |
121 | public function queryAndRun(
122 | callable $callback,
123 | $keyConditions,
124 | array $fieldsMapping,
125 | array $paramsMapping,
126 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
127 | $filterExpression = '',
128 | $isConsistentRead = false,
129 | $isAscendingOrder = true,
130 | $projectedFields = []
131 | ) {
132 | $this->getDynamodbTable()->queryAndRun(
133 | $callback,
134 | $keyConditions,
135 | $fieldsMapping,
136 | $paramsMapping,
137 | $indexName,
138 | $filterExpression,
139 | $isConsistentRead,
140 | $isAscendingOrder,
141 | $projectedFields
142 | );
143 | }
144 |
145 | public function queryCount(
146 | $keyConditions,
147 | array $fieldsMapping,
148 | array $paramsMapping,
149 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
150 | $filterExpression = '',
151 | $isConsistentRead = false,
152 | $isAscendingOrder = true
153 | ) {
154 | return $this->getDynamodbTable()->queryCount(
155 | $keyConditions,
156 | $fieldsMapping,
157 | $paramsMapping,
158 | $indexName,
159 | $filterExpression,
160 | $isConsistentRead,
161 | $isAscendingOrder
162 | );
163 | }
164 |
165 | public function multiQueryAndRun(
166 | callable $callback,
167 | $hashKeyName,
168 | $hashKeyValues,
169 | $rangeKeyConditions,
170 | array $fieldsMapping,
171 | array $paramsMapping,
172 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
173 | $filterExpression = '',
174 | $evaluationLimit = 30,
175 | $isConsistentRead = false,
176 | $isAscendingOrder = true,
177 | $concurrency = 10,
178 | $projectedFields = []
179 | ) {
180 | $this->getDynamodbTable()->multiQueryAndRun(
181 | $callback,
182 | $hashKeyName,
183 | $hashKeyValues,
184 | $rangeKeyConditions,
185 | $fieldsMapping,
186 | $paramsMapping,
187 | $indexName,
188 | $filterExpression,
189 | $evaluationLimit,
190 | $isConsistentRead,
191 | $isAscendingOrder,
192 | $concurrency,
193 | $projectedFields
194 | );
195 | }
196 |
197 | public function scan(
198 | $filterExpression = '',
199 | array $fieldsMapping = [],
200 | array $paramsMapping = [],
201 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
202 | &$lastKey = null,
203 | $evaluationLimit = 30,
204 | $isConsistentRead = false,
205 | $isAscendingOrder = true,
206 | $projectedFields = []
207 | ) {
208 | return $this->getDynamodbTable()->scan(
209 | $filterExpression,
210 | $fieldsMapping,
211 | $paramsMapping,
212 | $indexName,
213 | $lastKey,
214 | $evaluationLimit,
215 | $isConsistentRead,
216 | $isAscendingOrder,
217 | $projectedFields
218 | );
219 | }
220 |
221 | public function scanAndRun(
222 | callable $callback,
223 | $filterExpression = '',
224 | array $fieldsMapping = [],
225 | array $paramsMapping = [],
226 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
227 | $isConsistentRead = false,
228 | $isAscendingOrder = true,
229 | $projectedFields = []
230 | ) {
231 | $this->getDynamodbTable()->scanAndRun(
232 | $callback,
233 | $filterExpression,
234 | $fieldsMapping,
235 | $paramsMapping,
236 | $indexName,
237 | $isConsistentRead,
238 | $isAscendingOrder,
239 | $projectedFields
240 | );
241 | }
242 |
243 | public function parallelScanAndRun(
244 | $parallel,
245 | callable $callback,
246 | $filterExpression = '',
247 | array $fieldsMapping = [],
248 | array $paramsMapping = [],
249 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
250 | $isConsistentRead = false,
251 | $isAscendingOrder = true,
252 | $projectedFields = []
253 | ) {
254 | $this->getDynamodbTable()->parallelScanAndRun(
255 | $parallel,
256 | $callback,
257 | $filterExpression,
258 | $fieldsMapping,
259 | $paramsMapping,
260 | $indexName,
261 | $isConsistentRead,
262 | $isAscendingOrder,
263 | $projectedFields
264 | );
265 | }
266 |
267 | public function scanCount(
268 | $filterExpression = '',
269 | array $fieldsMapping = [],
270 | array $paramsMapping = [],
271 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
272 | $isConsistentRead = false,
273 | $parallel = 10
274 | ) {
275 | return $this->getDynamodbTable()->scanCount(
276 | $filterExpression,
277 | $fieldsMapping,
278 | $paramsMapping,
279 | $indexName,
280 | $isConsistentRead,
281 | $parallel
282 | );
283 | }
284 |
285 | /**
286 | * @param ItemManager $im
287 | * @param $classReflections
288 | * @param callable|null $outputFunction
289 | * @return DynamoDbSchemaTool
290 | */
291 | public function getSchemaTool(ItemManager $im, $classReflections, callable $outputFunction = null)
292 | {
293 | return new DynamoDbSchemaTool($im, $classReflections, $outputFunction);
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/src/ItemReflection.php:
--------------------------------------------------------------------------------
1 | cas type
45 | */
46 | protected $casProperties;
47 | /**
48 | * @var PartitionedHashKey[]
49 | * partitioned hash keys, in the format of property name => partitioned hash key definition
50 | */
51 | protected $partitionedHashKeys;
52 | /**
53 | * @var Field[]
54 | * Maps class property name to its field definition
55 | */
56 | protected $fieldDefinitions;
57 | /**
58 | * @var ReflectionProperty[]
59 | * Maps each class property name to its reflection property
60 | */
61 | protected $reflectionProperties;
62 | /**
63 | * @var array
64 | * Reserved attribute names will be cleared when hydrating an object
65 | */
66 | protected $reservedAttributeNames;
67 |
68 | public function __construct($itemClass, $reservedAttributeNames)
69 | {
70 | $this->itemClass = $itemClass;
71 | $this->reservedAttributeNames = $reservedAttributeNames;
72 | }
73 |
74 | public function dehydrate($obj)
75 | {
76 | if (!is_object($obj)) {
77 | throw new ODMException("You may only dehydrate an object!");
78 | }
79 |
80 | if (!$obj instanceof $this->itemClass) {
81 | throw new ODMException(
82 | "Object dehydrated is not of correct type, expected: " . $this->itemClass . ", got: " . get_class($obj)
83 | );
84 | }
85 |
86 | $array = [];
87 | foreach ($this->fieldDefinitions as $propertyName => $field) {
88 | $value = $this->getPropertyValue($obj, $propertyName);
89 | if (is_null($value) && $field->gsiKey) {
90 | continue;
91 | }
92 | $key = $field->name ? : $propertyName;
93 | $array[$key] = $value;
94 | }
95 |
96 | return $array;
97 | }
98 |
99 | public function hydrate(array $array, $obj = null)
100 | {
101 | if ($obj === null) {
102 | $obj = $this->getReflectionClass()->newInstanceWithoutConstructor();
103 | }
104 | elseif (!is_object($obj) || !$obj instanceof $this->itemClass) {
105 | throw new ODMException("You can not hydrate an object of wrong type, expected: " . $this->itemClass);
106 | }
107 |
108 | foreach ($array as $key => $value) {
109 | if (in_array($key, $this->reservedAttributeNames)) {
110 | // this attribute is reserved for other use
111 | continue;
112 | }
113 | if (!isset($this->propertyMapping[$key])) {
114 | // this property is not defined, skip it
115 | mwarning("Got an unknown attribute: %s with value %s", $key, print_r($value, true));
116 | continue;
117 | }
118 | $propertyName = $this->propertyMapping[$key];
119 | $fieldDefinition = $this->fieldDefinitions[$propertyName];
120 | if ($fieldDefinition->type == "string") {
121 | // cast to string because dynamo stores "" as null
122 | $value = strval($value);
123 | }
124 | $this->updateProperty($obj, $propertyName, $value);
125 | }
126 |
127 | return $obj;
128 | }
129 |
130 | public function parse(Reader $reader)
131 | {
132 | // initialize class annotation info
133 | $this->reflectionClass = new ReflectionClass($this->itemClass);
134 | $this->itemDefinition = $reader->getClassAnnotation($this->reflectionClass, Item::class);
135 | if (!$this->itemDefinition) {
136 | throw new NotAnnotatedException("Class " . $this->itemClass . " is not configured as an Item");
137 | }
138 |
139 | // initialize property annotation info
140 | $this->propertyMapping = [];
141 | $this->fieldDefinitions = [];
142 | $this->reflectionProperties = [];
143 | $this->attributeTypes = [];
144 | $this->casProperties = [];
145 | $this->partitionedHashKeys = [];
146 | foreach ($this->reflectionClass->getProperties() as $reflectionProperty) {
147 | if ($reflectionProperty->isStatic()) {
148 | continue;
149 | }
150 | $propertyName = $reflectionProperty->getName();
151 | $this->reflectionProperties[$propertyName] = $reflectionProperty;
152 |
153 | /** @var Field $field */
154 | $field = $reader->getPropertyAnnotation($reflectionProperty, Field::class);
155 | if (!$field) {
156 | continue;
157 | }
158 | $fieldName = $field->name ? : $propertyName;
159 | $this->propertyMapping[$fieldName] = $propertyName;
160 | $this->fieldDefinitions[$propertyName] = $field;
161 | $this->attributeTypes[$fieldName] = $field->type;
162 | if ($field->cas != Field::CAS_DISABLED) {
163 | $this->casProperties[$propertyName] = $field->cas;
164 | }
165 |
166 | /** @var PartitionedHashKey $partitionedHashKeyDef */
167 | $partitionedHashKeyDef = $reader->getPropertyAnnotation($reflectionProperty, PartitionedHashKey::class);
168 | if ($partitionedHashKeyDef) {
169 | $this->partitionedHashKeys[$propertyName] = $partitionedHashKeyDef;
170 | }
171 | }
172 | }
173 |
174 | public function getAllPartitionedValues($hashKeyName, $baseValue)
175 | {
176 | if (!isset($this->partitionedHashKeys[$hashKeyName])) {
177 | // mdebug("The field %s is not declared as a PartitionedHashKey!", $hashKeyName)
178 | return [$baseValue];
179 | }
180 |
181 | $def = $this->partitionedHashKeys[$hashKeyName];
182 | $ret = [];
183 | for ($i = 0; $i < $def->size; ++$i) {
184 | $ret[] = sprintf("%s-%s", $baseValue, dechex($i));
185 | }
186 |
187 | return $ret;
188 | }
189 |
190 | public function getPropertyValue($obj, $propertyName)
191 | {
192 | if (!$obj instanceof $this->itemClass) {
193 | throw new ODMException(
194 | "Object accessed is not of correct type, expected: " . $this->itemClass . ", got: " . get_class($obj)
195 | );
196 | }
197 |
198 | if (!isset($this->reflectionProperties[$propertyName])) {
199 | throw new ODMException(
200 | "Object " . $this->itemClass . " doesn't have a property named: " . $propertyName
201 | );
202 | }
203 | $reflectionProperty = $this->reflectionProperties[$propertyName];
204 | $oldAccessibility = $reflectionProperty->isPublic();
205 | $reflectionProperty->setAccessible(true);
206 | $ret = $reflectionProperty->getValue($obj);
207 | $reflectionProperty->setAccessible($oldAccessibility);
208 |
209 | return $ret;
210 | }
211 |
212 | public function updateProperty($obj, $propertyName, $value)
213 | {
214 | if (!$obj instanceof $this->itemClass) {
215 | throw new ODMException(
216 | "Object updated is not of correct type, expected: " . $this->itemClass . ", got: " . get_class($obj)
217 | );
218 | }
219 |
220 | if (!isset($this->reflectionProperties[$propertyName])) {
221 | throw new ODMException(
222 | "Object " . $this->itemClass . " doesn't have a property named: " . $propertyName
223 | );
224 | }
225 | $reflectionProperty = $this->reflectionProperties[$propertyName];
226 | $oldAccessibility = $reflectionProperty->isPublic();
227 | $reflectionProperty->setAccessible(true);
228 | $reflectionProperty->setValue($obj, $value);
229 | $reflectionProperty->setAccessible($oldAccessibility);
230 | }
231 |
232 | /**
233 | * @return mixed
234 | */
235 | public function getAttributeTypes()
236 | {
237 | return $this->attributeTypes;
238 | }
239 |
240 | /**
241 | * @return array
242 | */
243 | public function getCasProperties()
244 | {
245 | return $this->casProperties;
246 | }
247 |
248 | /**
249 | * Returns field name (attribute key for dynamodb) according to property name
250 | *
251 | * @param $propertyName
252 | *
253 | * @return string
254 | */
255 | public function getFieldNameByPropertyName($propertyName)
256 | {
257 | $field = $this->fieldDefinitions[$propertyName];
258 |
259 | return $field->name ? : $propertyName;
260 | }
261 |
262 | /**
263 | * @return array a map of property name to attribute key
264 | */
265 | public function getFieldNameMapping()
266 | {
267 | $ret = [];
268 | foreach ($this->fieldDefinitions as $propertyName => $field) {
269 | $ret[$propertyName] = $field->name ? : $propertyName;
270 | }
271 |
272 | return $ret;
273 | }
274 |
275 | public function getProjectedAttributes()
276 | {
277 | if ($this->getItemDefinition()->projected) {
278 | return array_keys($this->propertyMapping);
279 | }
280 | else {
281 | return [];
282 | }
283 | }
284 |
285 | /**
286 | * @return mixed
287 | */
288 | public function getItemClass()
289 | {
290 | return $this->itemClass;
291 | }
292 |
293 | /**
294 | * @return Item
295 | */
296 | public function getItemDefinition()
297 | {
298 | return $this->itemDefinition;
299 | }
300 |
301 | /**
302 | * @return PartitionedHashKey[]
303 | */
304 | public function getPartitionedHashKeys()
305 | {
306 | return $this->partitionedHashKeys;
307 | }
308 |
309 | public function getPrimaryIdentifier($obj)
310 | {
311 | $id = '';
312 | foreach ($this->getPrimaryKeys($obj) as $key => $value) {
313 | $id .= md5($value);
314 | }
315 |
316 | return md5($id);
317 | }
318 |
319 | public function getPrimaryKeys($obj, $asAttributeKeys = true)
320 | {
321 | $keys = [];
322 | foreach ($this->itemDefinition->primaryIndex->getKeys() as $key) {
323 | if (!isset($this->fieldDefinitions[$key])) {
324 | throw new AnnotationParsingException("Primary field " . $key . " is not defined.");
325 | }
326 | $attributeKey = $this->fieldDefinitions[$key]->name ? : $key;
327 |
328 | if (is_array($obj)) {
329 | if (!isset($obj[$attributeKey])) {
330 | throw new ODMException(
331 | "Cannot get identifier for incomplete object! <" . $attributeKey . "> is empty!"
332 | );
333 | }
334 | $value = $obj[$attributeKey];
335 | }
336 | else {
337 | $value = $this->getPropertyValue($obj, $key);
338 | }
339 |
340 | if ($asAttributeKeys) {
341 | $keys[$attributeKey] = $value;
342 | }
343 | else {
344 | $keys[$key] = $value;
345 | }
346 | }
347 |
348 | return $keys;
349 | }
350 |
351 | /**
352 | * @return ReflectionClass
353 | */
354 | public function getReflectionClass()
355 | {
356 | return $this->reflectionClass;
357 | }
358 |
359 | public function getRepositoryClass()
360 | {
361 | return $this->itemDefinition->repository ? : ItemRepository::class;
362 | }
363 |
364 | public function getTableName()
365 | {
366 | return $this->itemDefinition->table;
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/src/DBAL/Schema/DynamoDbSchemaTool.php:
--------------------------------------------------------------------------------
1 | getItemManager()->getDynamodbConfig());
31 |
32 | $classes = $this->getManagedItemClasses();
33 | foreach ($classes as $class => $reflection) {
34 | $tableName = $this->itemManager->getDefaultTablePrefix().$reflection->getTableName();
35 | if ($dynamoManager->listTables(sprintf("/^%s\$/", preg_quote($tableName, "/")))) {
36 | if (!$skipExisting && !$dryRun) {
37 | throw new ODMException("Table ".$tableName." already exists!");
38 | }
39 | }
40 | }
41 |
42 | $waits = [];
43 |
44 | /**
45 | * @var $class
46 | * @var ItemReflection $reflection
47 | */
48 | foreach ($classes as $class => $reflection) {
49 | $itemDef = $reflection->getItemDefinition();
50 | if ($itemDef->projected) {
51 | $this->outputWrite(sprintf("Class %s is projected class, will not create table.", $class));
52 | continue;
53 | }
54 |
55 | $attributeTypes = $reflection->getAttributeTypes();
56 | $fieldNameMapping = $reflection->getFieldNameMapping();
57 |
58 | $lsis = [];
59 | /** @var Index $localSecondaryIndex */
60 | foreach ($itemDef->localSecondaryIndices as $localSecondaryIndex) {
61 | $lsis[] = $localSecondaryIndex->getDynamodbIndex($fieldNameMapping, $attributeTypes);
62 | }
63 | $gsis = [];
64 | /** @var Index $globalSecondaryIndex */
65 | foreach ($itemDef->globalSecondaryIndices as $globalSecondaryIndex) {
66 | $gsis[] = $globalSecondaryIndex->getDynamodbIndex($fieldNameMapping, $attributeTypes);
67 | }
68 |
69 | $tableName = $this->itemManager->getDefaultTablePrefix().$reflection->getTableName();
70 |
71 | $this->outputWrite("Will create table $tableName for class $class ...");
72 | if (!$dryRun) {
73 | $dynamoManager->createTable(
74 | $tableName,
75 | $itemDef->primaryIndex->getDynamodbIndex($fieldNameMapping, $attributeTypes),
76 | $lsis,
77 | $gsis
78 | );
79 |
80 | if ($gsis) {
81 | // if there is gsi, we nee to wait before creating next table
82 | $this->outputWrite("Will wait for GSI creation ...");
83 | $dynamoManager->waitForTablesToBeFullyReady($tableName, 60, 2);
84 | }
85 | else {
86 | $waits[] = $dynamoManager->waitForTableCreation(
87 | $tableName,
88 | 60,
89 | 1,
90 | false
91 | );
92 | }
93 | $this->outputWrite('Created.');
94 | }
95 | }
96 |
97 | if (!$dryRun) {
98 | $this->outputWrite("Waiting for all tables to be active ...");
99 | all($waits)->wait();
100 | $this->outputWrite("Done.");
101 | }
102 | }
103 |
104 | /** @noinspection PhpStatementHasEmptyBodyInspection */
105 | public function updateSchema($isDryRun)
106 | {
107 | $dynamoManager = new DynamoDbManager($this->getItemManager()->getDynamodbConfig());
108 | $classes = $this->getManagedItemClasses();
109 | $classCreation = [];
110 | $gsiChanges = [];
111 | $im = $this->itemManager;
112 |
113 | /** @var ItemReflection $reflection */
114 | foreach ($classes as $class => $reflection) {
115 | if ($reflection->getItemDefinition()->projected) {
116 | // will skip projected table
117 | continue;
118 | }
119 | $tableName = $this->itemManager->getDefaultTablePrefix().$reflection->getTableName();
120 |
121 | if (!$dynamoManager->listTables(sprintf("/^%s\$/", preg_quote($tableName, "/")))) {
122 | // will create
123 | $classCreation[] = function () use ($isDryRun, $im, $class, $reflection, $dynamoManager, $tableName) {
124 | $itemDef = $reflection->getItemDefinition();
125 | $attributeTypes = $reflection->getAttributeTypes();
126 | $fieldNameMapping = $reflection->getFieldNameMapping();
127 |
128 | $lsis = [];
129 | foreach ($itemDef->localSecondaryIndices as $localSecondaryIndex) {
130 | $lsis[] = $localSecondaryIndex->getDynamodbIndex($fieldNameMapping, $attributeTypes);
131 | }
132 | $gsis = [];
133 | foreach ($itemDef->globalSecondaryIndices as $globalSecondaryIndex) {
134 | $gsis[] = $globalSecondaryIndex->getDynamodbIndex($fieldNameMapping, $attributeTypes);
135 | }
136 |
137 | $tableName = $im->getDefaultTablePrefix().$reflection->getTableName();
138 |
139 | $this->outputWrite("Will create table $tableName for class $class.");
140 |
141 | if (!$isDryRun) {
142 | $dynamoManager->createTable(
143 | $tableName,
144 | $itemDef->primaryIndex->getDynamodbIndex($fieldNameMapping, $attributeTypes),
145 | $lsis,
146 | $gsis
147 | );
148 | $this->outputWrite('Created.');
149 | }
150 |
151 | return $tableName;
152 | };
153 | }
154 | else {
155 | // will update
156 | $table = new DynamoDbTable($this->getItemManager()->getDynamodbConfig(), $tableName);
157 | $itemDef = $reflection->getItemDefinition();
158 | $attributeTypes = $reflection->getAttributeTypes();
159 | $fieldNameMapping = $reflection->getFieldNameMapping();
160 |
161 | $oldPrimaryIndex = $table->getPrimaryIndex();
162 | $primaryIndex = $itemDef->primaryIndex->getDynamodbIndex($fieldNameMapping, $attributeTypes);
163 |
164 | if (!$oldPrimaryIndex->equals($primaryIndex)) {
165 | throw new ODMException(
166 | sprintf(
167 | "Primary index changed, which is not possible when table is already created! [Table = %s]",
168 | $tableName
169 | )
170 | );
171 | }
172 |
173 | $oldLsis = $table->getLocalSecondaryIndices();
174 | foreach ($itemDef->localSecondaryIndices as $localSecondaryIndex) {
175 | $idx = $localSecondaryIndex->getDynamodbIndex($fieldNameMapping, $attributeTypes);
176 | if (!isset($oldLsis[$idx->getName()])) {
177 | throw new ODMException(
178 | sprintf(
179 | "LSI named %s did not exist, you cannot update LSI when table is created! [Table = %s]",
180 | $idx->getName(),
181 | $tableName
182 | )
183 | );
184 | }
185 | else {
186 | unset($oldLsis[$idx->getName()]);
187 | }
188 | }
189 | if ($oldLsis) {
190 | throw new ODMException(
191 | sprintf(
192 | "LSI named %s removed, you cannot remove any LSI when table is created!",
193 | implode(",", array_keys($oldLsis))
194 | )
195 | );
196 | }
197 |
198 | $oldGsis = $table->getGlobalSecondaryIndices();
199 | foreach ($itemDef->globalSecondaryIndices as $globalSecondaryIndex) {
200 | $idx = $globalSecondaryIndex->getDynamodbIndex($fieldNameMapping, $attributeTypes);
201 |
202 | if (!isset($oldGsis[$idx->getName()])) {
203 | // new GSI
204 | $gsiChanges[] = function () use (
205 | $isDryRun,
206 | $dynamoManager,
207 | $class,
208 | $tableName,
209 | $table,
210 | $idx
211 | ) {
212 | $this->outputWrite(
213 | "Will add GSI ["
214 | .$idx->getName()
215 | ."] to table $tableName for class $class ..."
216 | );
217 | if (!$isDryRun) {
218 | $table->addGlobalSecondaryIndex($idx);
219 | // if there is gsi alteration, we nee to wait before continue
220 | $this->outputWrite("Will wait for creation of GSI ".$idx->getName()." ...");
221 | $dynamoManager->waitForTablesToBeFullyReady($tableName, 300, 5);
222 | $this->outputWrite('Done.');
223 | }
224 |
225 | return $tableName;
226 | };
227 | }
228 | else {
229 | // GSI with same name
230 |
231 | if ($idx->equals($oldGsis[$idx->getName()])) {
232 | // nothing to update
233 | }
234 | else {
235 | $gsiChanges[] = function () use (
236 | $isDryRun,
237 | $dynamoManager,
238 | $class,
239 | $tableName,
240 | $table,
241 | $idx
242 | ) {
243 | $this->outputWrite(
244 | "Will update GSI ["
245 | .$idx->getName()
246 | ."] on table $tableName for class $class ..."
247 | );
248 | if (!$isDryRun) {
249 | // if there is gsi alteration, we nee to wait before continue
250 | $table->deleteGlobalSecondaryIndex($idx->getName());
251 | $this->outputWrite("Will wait for deletion of GSI ".$idx->getName()." ...");
252 | $dynamoManager->waitForTablesToBeFullyReady($tableName, 300, 5);
253 | //$output->writeln(
254 | // "Will sleep 3 seconds before creating new GSI. If the creation fails, you can feel free to run update command again."
255 | //);
256 | //sleep(3);
257 | $table->addGlobalSecondaryIndex($idx);
258 | $this->outputWrite("Will wait for creation of GSI ".$idx->getName()." ...");
259 | $dynamoManager->waitForTablesToBeFullyReady($tableName, 300, 5);
260 | $this->outputWrite('Done.');
261 | }
262 |
263 | return $tableName;
264 | };
265 | }
266 |
267 | unset($oldGsis[$idx->getName()]);
268 | }
269 | }
270 | if ($oldGsis) {
271 | /** @var DynamoDbIndex $removedGsi */
272 | foreach ($oldGsis as $removedGsi) {
273 | $gsiChanges[] = function () use (
274 | $isDryRun,
275 | $dynamoManager,
276 | $class,
277 | $tableName,
278 | $table,
279 | $removedGsi
280 | ) {
281 | $this->outputWrite(
282 | "Will remove GSI ["
283 | .$removedGsi->getName()
284 | ."] from table $tableName for class $class ..."
285 | );
286 | if (!$isDryRun) {
287 | $table->deleteGlobalSecondaryIndex($removedGsi->getName());
288 | $this->outputWrite("Will wait for deletion of GSI ".$removedGsi->getName()." ...");
289 | $dynamoManager->waitForTablesToBeFullyReady($tableName, 300, 5);
290 | $this->outputWrite('Done.');
291 | }
292 |
293 | return $tableName;
294 | };
295 | }
296 | }
297 | }
298 | }
299 |
300 | if (!$classCreation && !$gsiChanges) {
301 | $this->outputWrite("Nothing to change.");
302 | }
303 | else {
304 | $waits = [];
305 | foreach ($classCreation as $callable) {
306 | $tableName = call_user_func($callable);
307 | if (!$isDryRun) {
308 | $waits[] = $dynamoManager->waitForTableCreation($tableName, 60, 1, false);
309 | }
310 | }
311 | if ($waits) {
312 | $this->outputWrite("Waiting for all created tables to be active ...");
313 | all($waits)->wait();
314 | $this->outputWrite("Done.");
315 | }
316 |
317 | $changedTables = [];
318 | foreach ($gsiChanges as $callable) {
319 | $tableName = call_user_func($callable);
320 | if (!$isDryRun) {
321 | $changedTables[] = $tableName;
322 | }
323 | }
324 | }
325 | }
326 |
327 | public function dropSchema()
328 | {
329 | $dynamoManager = new DynamoDbManager($this->getItemManager()->getDynamodbConfig());
330 | $classes = $this->getManagedItemClasses();
331 | $im = $this->getItemManager();
332 |
333 | $waits = [];
334 | foreach ($classes as $class => $reflection) {
335 | $tableName = $im->getDefaultTablePrefix().$reflection->getTableName();
336 | $this->outputWrite("Will drop table $tableName for class $class ...");
337 | try {
338 | $dynamoManager->deleteTable($tableName);
339 | } catch (DynamoDbException $e) {
340 | if ("ResourceNotFoundException" == $e->getAwsErrorCode()) {
341 | $this->outputWrite('Table not found.');
342 | }
343 | else {
344 | throw $e;
345 | }
346 | }
347 | $waits[] = $dynamoManager->waitForTableDeletion(
348 | $tableName,
349 | 60,
350 | 1,
351 | false
352 | );
353 | $this->outputWrite('Deleted.');
354 | }
355 | $this->outputWrite("Waiting for all tables to be inactive");
356 | all($waits)->wait();
357 | $this->outputWrite("Done.");
358 | }
359 | }
360 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Object Data Mapping component for DynamoDb
2 |
3 | The oasis/dynamodb-odm is an ODM (object data mapping) library for easy use of AWS' powerful key-value database: DynamoDb.
4 |
5 | > **NOTE**: this document assumes you have some understanding of what DynamoDB is and the difference between DynamoDB and traditional RDBMS (e.g. MySQL). Some terms and ideas discussed in this document are DynamoDB specific and will not be explained in this documentation. To study DynamoDB, please refer to the [official dev guide](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide)
6 |
7 | ## Installation & Configuration
8 |
9 | To get oasis/dynamodb-odm, you can simple require it via `composer`:
10 |
11 | ```bash
12 | $ composer require oasis/dynamodb-odm
13 | ```
14 |
15 | ### Class Loading
16 |
17 | Autoloading for DynamoDb ODM is taken care of by `composer`. You just need to include the composer autoload file in your project:
18 |
19 | ```php
20 | "oasis-minhao",
35 | "region" => "ap-northeast-1"
36 | ];
37 | $tablePrefix = "odm-";
38 | $cacheDir = __DIR__ . "/ut/cache";
39 | $isDev = true;
40 | $itemNamespace = 'Oasis\Mlib\ODM\Dynamodb\Ut'; // in practice, this usually looks like: My\Root\Namespace\Items
41 | $itemSrcDir = __DIR__ . "/ut"; // in practice, this usually points to src/Items directory
42 |
43 | $im = new ItemManager(
44 | $awsConfig,
45 | $tablePrefix,
46 | $cacheDir,
47 | $isDev
48 | );
49 | $im->addNamespace(
50 | $itemNamespace,
51 | $itemSrcDir
52 | );
53 |
54 | ```
55 |
56 | The explanation of each argument can be found below:
57 |
58 | argument | description | default value
59 | --- | --- | ---
60 | awsConfig | configuration array for aws SDK, `profile` and `region` are mandatory. | **mandatory**
61 | tablePrefix | a prefix to table names | **mandatory**
62 | cacheDir | cache direcotry to store metadata | **mandatory**
63 | isDev | is development environment or not. Under dev environment, changes to Item class will automatically invalidate cached metadata. Under production environment, this has to be done manually. | `true`
64 | itemSrcDir | a source directory under which Item classes can be found | **mandatory**
65 | itemNamespace | the base namespace for the managed Item classes source directory | **mandatory**
66 |
67 | > **NOTE**: an Item class defines a type of item managed by ODM. Some typical examples are: User, Order, GameRoom, and Card
68 |
69 | ### Setting Up Command Line Tool
70 |
71 | DynamoDb ODM ships with a number of command line tools that are very helpful during development. You can call this command from the Composer binary directory:
72 |
73 | ```bash
74 | $ ./vendor/bin/oasis-dynamodb-odm
75 | ```
76 |
77 | You need to register your application's `ItemManager` to the console tool to make use of the built-in command. This is done by creating an **odm-config.php** file under the calling directory, with the following content:
78 |
79 | ```php
80 | **NOTE**: Check-and-set validation is done only when you call `ItemManger#flush()`. Failure to meet the check and set condition(s) will lead to an `Oasis\Mlib\ODM\Dynamodb\Exceptions\DataConsistencyException` being thrown.
217 |
218 | ## Working with Objects
219 |
220 | All objects (items) in ODM are managed. Operations on objects are managed like object-level transaction. Once an object is managed, either by persisted as a new object or fetched from database, its managed state is stored in the ItemManager. Any change to the object will be recorded in memory. Changes to object can then be commited by invoking the `ItemManager#flush()` method on the ItemManager.
221 |
222 | The ItemManager can be manually cleared by calling `ItemManager#clear()`. However, any changes that are not committed yet will be lost.
223 |
224 | > **NOTE**: it is very **important** to understand, that only `ItemManager#flush()` will cause write operations against the database. Any other methods such as `ItemManager#persist($item)` or `ItemManager#remove($item)` only notify the ItemManager to perform these operations during flush. Not calling `ItemManager#flush()` will lead to all changes during that request being lost.
225 |
226 | #### Persisting Item
227 |
228 | An item can be made persistent by passing it to the `ItemManager#persist($item)` method. By applying the persist operation on some item, that item becomes **MANAGED**, which means that its persistence is from now on managed by an ItemManager. As a result the persistent state of such an item will subsequently be properly synchronized with the database when `ItemManager#flush()` is invoked.
229 |
230 | Example:
231 |
232 | ```php
233 | setName('Mr.Right');
237 | $im->persist($user);
238 | $im->flush();
239 |
240 | ```
241 |
242 | #### Removing Item
243 |
244 | An item can be removed from persistent storage by passing it to the `ItemManager#remove($item)` method. By applying the remove operation on some item, that item becomes **REMOVED**, which means that its persistent state will be deleted once `ItemManager#flush()` is invoked.
245 |
246 | Example:
247 |
248 | ```php
249 | remove($user);
253 | $im->flush();
254 |
255 | ```
256 |
257 | #### Detaching Item
258 |
259 | An item is detached from an ItemManager and thus no longer managed by invoking the `ItemManager#detach($item)` method on it. Changes made to the detached item, if any (including removal of the item), will not be synchronized to the database after the item has been detached.
260 |
261 | DynamoDb ODM will not hold on to any references to a detached item.
262 |
263 |
264 | Example:
265 |
266 | ```php
267 | detach($user);
271 | $user->setName('Mr.Left');
272 | $im->flush(); // changes to $user will not be synchronized
273 |
274 | ```
275 |
276 | #### Synchronization with the Database
277 |
278 | The state of persistent items is synchronized with the database on `flush()` of an ItemManager. The synchronization involves writing any updates to persistent items to the database. When `ItemManager#flush()` is called, ODM inspects all managed, new and removed items and will perform the following operations:
279 |
280 | - create new object in database
281 | - update changed attributes for managed items in database
282 | - delete removed item from database
283 |
284 | ## Fetching Item(s)
285 |
286 | DynamoDb ODM provides the following ways, in increasing level of power and flexibility, to fetch persistent object(s). You should always start with the simplest one that suits your needs.
287 |
288 | #### By Primary Index
289 |
290 | The most basic way to fetch a persistent object is by its primary index using the `ItemManager#get($itemClass, $primayKeys)` method. Here is an example:
291 |
292 | ```php
293 | get(User::class, ["id" => 1]);
296 |
297 | ```
298 |
299 | The return value is either the found item instance or null if no instance could be found with the given identifier.
300 |
301 | Essentially, `ItemManager#get()` is just a shortcut for the following:
302 |
303 | ```php
304 | /** @var ItemManager $im */
305 | /** @var ItemRepository $userRepo */
306 | $userRepo = $im->getRepository(User::class);
307 | $user = $userRepo->get(["id" => 1]);
308 |
309 | ```
310 |
311 | #### By Simple Conditions on Queriable Index
312 |
313 | To query for one or more items based on simple conditions, use the `ItemManager#query()` and `ItemManager#queryAndRun()` methods on a repository as follows:
314 |
315 | ```php
316 | /** @var ItemManager $im */
317 | /** @var ItemRepository $userRepo */
318 | $userRepo = $im->getRepository(User::class);
319 | /** @var Users[] $users */
320 | $users = $userRepo->query(
321 | "#class = :class AND #age >= :minAge",
322 | [
323 | ":class" => "A",
324 | ":minAge" => 25,
325 | ],
326 | "class-age-index"
327 | );
328 |
329 | ```
330 |
331 | > **NOTE**: a simple condition is a condition that uses one and only one index. If the used index contains both _hash key_ and _range key_, the _range key_ may only be used when _hash key_ is also presented in the condition. Furthermore, only equal test operation can be performed against the _hash key_.
332 |
333 | #### By Using Multi Query on Partitioned Hash Key
334 |
335 | To query for one or more items based on a PartitionedHashKey, use `ItemManager#multiQueryAndRun()` methods on a repository as follows:
336 |
337 | ```php
338 | /** @var ItemManager $im */
339 | /** @var ItemRepository $userRepo */
340 | $userRepo = $im->getRepository(User::class);
341 | /** @var Users[] $users */
342 | $users = $userRepo->multiQueryAndRun(
343 | function ($item) {
344 | // each item returned can be accessed here in the callback
345 | },
346 | "classPartition", // PartitionedHashKey field name
347 | "A", // value expected in the base field (not the partition field)
348 | "#age >= :minAge", // only range conditions here
349 | [
350 | ":minAge" => 25,
351 | ],
352 | "class-partition-age-index" // index for PartitionedHashKey
353 | );
354 |
355 | ```
356 |
357 | #### By Filters on Non-Queriable Index
358 |
359 | To query for one or more items which has no associated index, use the `ItemManager#scan()` and `ItemManager#scanAndRun()` methods on a repository as follows:
360 |
361 | ```php
362 | /** @var ItemManager $im */
363 | /** @var ItemRepository $userRepo */
364 | $userRepo = $im->getRepository(User::class);
365 | /** @var Users[] $users */
366 | $users = $userRepo->scan(
367 | "#class = :class AND #age >= :minAge AND #name = :name",
368 | [
369 | ":class" => "A",
370 | ":minAge" => 25,
371 | ":name" => "John",
372 | ],
373 | );
374 |
375 | ```
376 |
377 | ## Using the Command Line Tool
378 |
379 | DynamoDb ODM ships an executable tool together with the library. After installation, there are following built-commands which helps you manage the database schema for the items:
380 |
381 | #### Create
382 |
383 | ```bash
384 | $ ./vendor/bin/oasis-dynamodb-odm odm:schema-tool:create
385 | ```
386 |
387 | The create command will iterate all managed items and create tables correspondingly. All primary index, LSIs and GSIs are created as well.
388 |
389 | > **NOTE**: if a table with the same name under the same prefix already exists, an exception will be thrown. No table will be created in this case.
390 |
391 | > **NOTE**: if you would like to skip creating existing table (i.e. only create non-existing tables), you can use the "--skip-existing-table" option
392 |
393 | #### Update
394 |
395 | ```bash
396 | $ ./vendor/bin/oasis-dynamodb-odm odm:schema-tool:update
397 | ```
398 |
399 | The update command is actually a more powerful (and slower too) version of create command. It checks all managed items and creates the table if it doesn't exist. Furthermore, if a table exists but have different GSIs defined, the update command will update the GSIs accordingly.
400 |
401 | > **NOTE**: due to the nature of DynamoDb, it is not possible to update the primary index or LSI when a table is already created. Under dev environment, it is suggested to drop the table and re-create them when needed.
402 |
403 | > **NOTE**: if you would like to only see the changes to database schemas without perfoming actual update, you can specify the "--dry-run" option in command line. The program will only prompts possible changes withou actually performing them.
404 |
405 | #### Drop
406 |
407 | ```bash
408 | $ ./vendor/bin/oasis-dynamodb-odm odm:schema-tool:drop
409 | ```
410 |
411 | The drop command will drop all tables associated with the managed items. **DO NOT** run this command in production environment!
412 |
--------------------------------------------------------------------------------
/ut/ItemManagerTest.php:
--------------------------------------------------------------------------------
1 | itemManager = new ItemManager(
29 | UTConfig::$dynamodbConfig, UTConfig::$tablePrefix, __DIR__ . "/cache", true
30 | );
31 | $this->itemManager2 = new ItemManager(
32 | new DynamoDbConnection(UTConfig::$dynamodbConfig), UTConfig::$tablePrefix, __DIR__ . "/cache", true
33 | );
34 | }
35 |
36 | public function testPersistAndGet()
37 | {
38 | $id = mt_rand(1000, PHP_INT_MAX);
39 | $user = new User();
40 | $user->setId($id);
41 | $user->setName('Alice');
42 | $this->itemManager->persist($user);
43 | $this->itemManager->flush();
44 |
45 | /** @var User $user2 */
46 | $user2 = $this->itemManager->get(User::class, ['id' => $id]);
47 |
48 | $this->assertEquals($user, $user2); // user object will be reused when same primary keys are used
49 | $this->assertEquals('Alice', $user2->getName());
50 |
51 | return $id;
52 | }
53 |
54 | /**
55 | * @depends testPersistAndGet
56 | *
57 | * @param $id
58 | */
59 | public function testDoublePersist($id)
60 | {
61 | $id2 = $id + 1;
62 | $user = new User();
63 | $user->setId($id2);
64 | $user->setName('Howard');
65 | $this->itemManager->persist($user);
66 |
67 | /** @var User $user2 */
68 | $user2 = $this->itemManager->get(User::class, ['id' => $id2]);
69 |
70 | $this->assertEquals($user, $user2); // user object will be reused when same primary keys are used
71 | $this->assertEquals('Howard', $user2->getName());
72 |
73 | /** @var User $user3 */
74 | $user3 = $this->itemManager->get(User::class, ['id' => $id2], true);
75 |
76 | $this->assertNull($user3);
77 | }
78 |
79 | /**
80 | * @depends testPersistAndGet
81 | *
82 | * @param $id
83 | * @return string
84 | */
85 | public function testEdit($id)
86 | {
87 | /** @var User $user */
88 | $user = $this->itemManager->get(User::class, ['id' => $id]);
89 | $this->assertInstanceOf(User::class, $user);
90 | $this->assertNotEquals('John', $user->getName());
91 | $user->setName('John');
92 | $user->haha = 22;
93 | $this->itemManager->flush();
94 |
95 | $this->itemManager->clear();
96 | /** @var User $user2 */
97 | $user2 = $this->itemManager->get(User::class, ['id' => $id]);
98 |
99 | $this->assertInstanceOf(User::class, $user2);
100 | $this->assertTrue($user !== $user2);
101 | $this->assertEquals('John', $user2->getName());
102 |
103 | return $id;
104 | }
105 |
106 | /**
107 | * @depends testEdit
108 | *
109 | * @param $id
110 | */
111 | public function testCASEnabled($id)
112 | {
113 | /** @var User $user */
114 | $user = $this->itemManager->get(User::class, ['id' => $id]);
115 | /** @var User $user2 */
116 | $user2 = $this->itemManager2->get(User::class, ['id' => $id]);
117 |
118 | $user->setName('Chris');
119 | $this->itemManager->flush();
120 |
121 | $user2->setName('Michael');
122 | self::expectException(DataConsistencyException::class);
123 | $this->itemManager2->flush();
124 | }
125 |
126 | /**
127 | * @depends testEdit
128 | *
129 | * @param $id
130 | * @return string
131 | */
132 | public function testCASTimestamp($id)
133 | {
134 | /** @var User $user */
135 | $user = $this->itemManager->get(User::class, ['id' => $id]);
136 | sleep(1);
137 | $user->setWage(777);
138 | $time = time();
139 | $this->itemManager->flush();
140 | $this->itemManager->clear();
141 |
142 | $user = $this->itemManager->get(User::class, ['id' => $id]);
143 | $lastUpdated = $user->getLastUpdated();
144 | $this->assertLessThanOrEqual(1, abs($lastUpdated - $time));
145 |
146 | return $id;
147 | }
148 |
149 | /**
150 | * @depends testCASTimestamp
151 | *
152 | * @param $id
153 | */
154 | public function testCreatingInconsistentData($id)
155 | {
156 | /** @var User $user */
157 | $user = $this->itemManager->get(User::class, ['id' => $id]);
158 | $this->itemManager->clear();
159 |
160 | //$user->setLastUpdated(0);
161 | //$user->setWage(999);
162 | $this->itemManager->persist($user);
163 |
164 | self::expectException(DataConsistencyException::class);
165 | $this->itemManager->flush();
166 |
167 | }
168 |
169 | /**
170 | * @depends testCASTimestamp
171 | *
172 | * @param $id
173 | */
174 | public function testUpdatingInconsistentDataWhenUsingCASTimestamp($id)
175 | {
176 | /** @var User $user */
177 | $user = $this->itemManager->get(User::class, ['id' => $id]);
178 | /** @var User $user2 */
179 | $user2 = $this->itemManager2->get(User::class, ['id' => $id]);
180 |
181 | //$user->setLastUpdated(time() + 10);
182 | sleep(1);
183 | $user->setAlias('emperor');
184 | $this->itemManager->flush();
185 | $user2->setWage(999);
186 |
187 | self::expectException(DataConsistencyException::class);
188 | $this->itemManager2->flush();
189 |
190 | }
191 |
192 | /**
193 | * @depends testCASTimestamp
194 | *
195 | * @param $id
196 | */
197 | public function testNoDoubleSetWhenFlushingTwice($id)
198 | {
199 | /** @var User $user */
200 | $user = $this->itemManager->get(User::class, ['id' => $id]);
201 | $user->setAlias('pope');
202 | $time = time();
203 | $this->itemManager->flush();
204 | sleep(2);
205 | $this->itemManager->flush();
206 | $lastUpdated = $user->getLastUpdated();
207 | $this->assertLessThanOrEqual(1, abs($lastUpdated - $time));
208 | }
209 |
210 | /**
211 | * @depends testCASTimestamp
212 | */
213 | public function testNoDoubleSetWhenInsertedAreFlushedTwice()
214 | {
215 | $id = mt_rand(1000, PHP_INT_MAX);
216 | $user = new User();
217 | $user->setId($id);
218 | $user->setName('Alice');
219 | $this->itemManager->persist($user);
220 | $time = time();
221 | $this->itemManager->flush();
222 | sleep(2);
223 | $this->itemManager->flush();
224 | $lastUpdated = $user->getLastUpdated();
225 | $this->assertLessThanOrEqual(1, abs($lastUpdated - $time));
226 |
227 | $this->itemManager->remove($user);
228 | $this->itemManager->flush();
229 | $this->itemManager->flush();
230 | }
231 |
232 | /**
233 | * @depends testCASTimestamp
234 | *
235 | * @param $id
236 | * @return string
237 | */
238 | public function testRefresh($id)
239 | {
240 | /** @var User $user */
241 | $user = $this->itemManager->get(User::class, ['id' => $id], true);
242 | $user->setWage(888);
243 | $this->itemManager->refresh($user);
244 |
245 | $this->assertEquals(777, $user->getWage());
246 |
247 | // unmanaged refresh will work when persist-if-not-managed is set to true
248 | $this->itemManager->clear();
249 | $user = new User();
250 | $user->setId($id);
251 | $user->setName('Mary');
252 | $user->setWage(999);
253 | $this->itemManager->refresh($user, true);
254 |
255 | $this->assertEquals(777, $user->getWage());
256 | $this->assertNotEquals('Mary', $user->getName());
257 |
258 | // refreshing detached object works too
259 | $this->itemManager->detach($user);
260 | $user = new User();
261 | $user->setId($id);
262 | $user->setName('Mary');
263 | $user->setWage(999);
264 | $this->itemManager->refresh($user, true);
265 |
266 | $this->assertEquals(777, $user->getWage());
267 | $this->assertNotEquals('Mary', $user->getName());
268 |
269 | $this->itemManager->flush();
270 | $this->itemManager->clear();
271 | $user = $this->itemManager->get(User::class, ['id' => $id], true);
272 | $this->assertEquals(777, $user->getWage());
273 |
274 | $user->setWage(888);
275 | $this->itemManager->flush();
276 | $this->itemManager->clear();
277 | $user = $this->itemManager->get(User::class, ['id' => $id], true);
278 | $this->assertEquals(888, $user->getWage());
279 |
280 | $user->setWage(777); // restore to 777 for other tests
281 | $this->itemManager->flush();
282 |
283 | return $id;
284 | }
285 |
286 | /**
287 | * @depends testRefresh
288 | *
289 | * @param $id
290 | */
291 | public function testRefreshingJustPersistedObject($id)
292 | {
293 | $this->itemManager->clear();
294 | $user = new User();
295 | $user->setId($id);
296 | $user->setName('Mary');
297 | $user->setWage(999);
298 | $this->itemManager->persist($user);
299 | $this->expectException(ODMException::class);
300 | $this->itemManager->refresh($user);
301 | }
302 |
303 | /**
304 | * @depends testRefresh
305 | *
306 | * @param $id
307 | */
308 | public function testRefreshingJustRemovedObject($id)
309 | {
310 | $this->itemManager->clear();
311 | $user = $this->itemManager->get(User::class, ['id' => $id], true);
312 | $this->itemManager->remove($user);
313 | $this->expectException(ODMException::class);
314 | $this->itemManager->refresh($user);
315 | }
316 |
317 | /**
318 | * @depends testRefresh
319 | *
320 | * @param $id
321 | * @return string
322 | */
323 | public function testDetach($id)
324 | {
325 | /** @var User $user */
326 | $user = $this->itemManager->get(User::class, ['id' => $id]);
327 | $user->setWage(888);
328 | $this->itemManager->detach($user);
329 | $this->itemManager->flush();
330 | $this->itemManager->clear();
331 | $user = $this->itemManager->get(User::class, ['id' => $id]);
332 | $this->assertEquals(777, $user->getWage());
333 |
334 | return $id;
335 | }
336 |
337 | /**
338 | * @depends testDetach
339 | *
340 | * @param $id
341 | */
342 | public function testDelete($id)
343 | {
344 | /** @var User $user */
345 | $user = $this->itemManager->get(User::class, ['id' => $id]);
346 | $this->assertInstanceOf(User::class, $user);
347 |
348 | $this->itemManager->remove($user);
349 | $this->itemManager->flush();
350 |
351 | $this->itemManager->clear();
352 | /** @var User $user2 */
353 | $user2 = $this->itemManager->get(User::class, ['id' => $id]);
354 | $this->assertNull($user2);
355 |
356 | $this->itemManager->persist($user);
357 | $this->itemManager->flush();
358 | $user2 = $this->itemManager->get(User::class, ['id' => $id]);
359 | $this->assertTrue($user2 instanceof User);
360 | $this->itemManager->getRepository(User::class)->removeById(['id' => $id]);
361 | $this->itemManager->flush();
362 | $this->itemManager->clear();
363 | $user2 = $this->itemManager->get(User::class, ['id' => $id]);
364 | $this->assertNull($user2);
365 |
366 | }
367 |
368 | public function testQueryAndScan()
369 | {
370 | $base = mt_rand(100, PHP_INT_MAX);
371 |
372 | $users = [];
373 | for ($i = 0; $i < 10; ++$i) {
374 | $id = $base + $i;
375 | $user = new User();
376 | $user->setId($id);
377 | $user->setName('Batch #' . ($i + 1));
378 | $user->setHometown(((($i % 2) == 0) ? 'LA' : 'NY') . $base);
379 | $user->setAge(46 + $i); // 46 to 55
380 | $user->setWage(12345);
381 | $users[] = $user;
382 | $this->itemManager->persist($user);
383 | }
384 |
385 | $this->itemManager->flush();
386 | $this->itemManager->clear();
387 |
388 | $count = $this->itemManager->getRepository(User::class)->queryCount(
389 | '#hometown = :hometown AND #age > :age',
390 | [':hometown' => 'NY' . $base, ':age' => 45],
391 | 'hometown-age-index'
392 | );
393 | $this->assertEquals(5, $count);
394 | $result = $this->itemManager->getRepository(User::class)->queryAll(
395 | '#hometown = :hometown AND #age > :age',
396 | [':hometown' => 'NY' . $base, ':age' => 45],
397 | 'hometown-age-index'
398 | );
399 | $this->assertEquals(5, count($result));
400 |
401 | $count = $this->itemManager->getRepository(User::class)->multiQueryCount(
402 | "hometownPartition",
403 | "NY" . $base,
404 | "#age > :age",
405 | [":age" => 48],
406 | "home-age-gsi"
407 | );
408 | $this->assertEquals(4, $count);
409 |
410 | $result = [];
411 | $this->itemManager->getRepository(User::class)->multiQueryAndRun(
412 | function ($item) use (&$result) {
413 | $result[] = $item;
414 | },
415 | "hometownPartition",
416 | "NY" . $base,
417 | "#age > :age",
418 | [":age" => 48],
419 | "home-age-gsi"
420 | );
421 | $this->assertEquals(4, count($result));
422 |
423 | // remove all inserted users
424 | $count = $this->itemManager->getRepository(User::class)->scanCount(
425 | '#wage = :wage AND #id BETWEEN :idmin AND :idmax ',
426 | [
427 | ':wage' => 12345,
428 | ':idmin' => $base,
429 | ':idmax' => $base + 10,
430 | ]
431 | );
432 | $this->assertEquals(10, $count);
433 | $count = 0;
434 | $this->itemManager->getRepository(User::class)->scanAndRun(
435 | function (User $user) use (&$count) {
436 | $count++;
437 | $this->itemManager->remove($user);
438 | },
439 | '#wage = :wage AND #id BETWEEN :idmin AND :idmax ',
440 | [
441 | ':wage' => 12345,
442 | ':idmin' => $base,
443 | ':idmax' => $base + 10,
444 | ],
445 | DynamoDbIndex::PRIMARY_INDEX,
446 | false,
447 | true,
448 | 5
449 | );
450 | $this->assertEquals(10, $count);
451 |
452 | $this->itemManager->flush();
453 | }
454 |
455 | public function testBatchNewWithCASDisabled()
456 | {
457 | $base = mt_rand(100, PHP_INT_MAX);
458 |
459 | /** @var User[] $users */
460 | $users = [];
461 | $keys = [];
462 | for ($i = 0; $i < 10; ++$i) {
463 | $id = $base + $i;
464 | $user = new User();
465 | $user->setId($id);
466 | $user->setName('Batch #' . ($i + 1));
467 | $user->setHometown(((($i % 2) == 0) ? 'LA' : 'NY') . $base);
468 | $user->setAge(46 + $i); // 46 to 55
469 | $user->setWage(12345);
470 | $users[$id] = $user;
471 | $this->itemManager->persist($user);
472 |
473 | $keys[] = ["id" => $id];
474 | }
475 |
476 | $this->itemManager->setSkipCheckAndSet(true);
477 | $this->itemManager->flush();
478 | $this->itemManager->setSkipCheckAndSet(false);
479 |
480 | return $users;
481 | }
482 |
483 | /**
484 | * @depends testBatchNewWithCASDisabled
485 | *
486 | * @param User[] $users
487 | */
488 | public function testBatchGet($users)
489 | {
490 | $keys[] = ["id" => -PHP_INT_MAX,]; // some non existing key
491 | $result = $this->itemManager->getRepository(User::class)->batchGet($keys);
492 | $this->assertEquals(count($keys), count($result) + 1); // we get all result except the non-existing one
493 | /** @var User $user */
494 | foreach ($result as $user) {
495 | $this->assertArrayHasKey($user->getId(), $users);
496 | $this->assertEquals($users[$user->getId()]->getName(), $user->getName());
497 | }
498 | }
499 |
500 | /**
501 | * @depends testBatchGet
502 | */
503 | public function testRemoveAll()
504 | {
505 | $this->itemManager->getRepository(User::class)->removeAll();
506 | $remaining = $this->itemManager->getRepository(User::class)->scanAll(
507 | '',
508 | [],
509 | DynamoDbIndex::PRIMARY_INDEX,
510 | true
511 | );
512 | $this->assertTrue($remaining->isEmpty(), json_encode($remaining));
513 | }
514 |
515 | public function testGetWithAttributeKey()
516 | {
517 | self::expectException(ODMException::class);
518 | $this->itemManager->get(User::class, ['uid' => 10]);
519 | }
520 |
521 | public function testQueryWithAttributeKey()
522 | {
523 | self::expectException(ODMException::class);
524 | $this->itemManager->getRepository(User::class)
525 | ->query(
526 | '#hometown = :hometown AND #salary > :wage',
527 | [':hometown' => 'NY', ':wage' => 100],
528 | 'hometown-salary-index'
529 | );
530 | }
531 |
532 | public function testScanWithAttributeKey()
533 | {
534 | self::expectException(ODMException::class);
535 | $this->itemManager->getRepository(User::class)
536 | ->scan(
537 | '#hometown = :hometown AND #salary > :wage',
538 | [':hometown' => 'NY', ':wage' => 100]
539 | );
540 | }
541 |
542 | public function testUnmanagedRemove()
543 | {
544 | $user = new User();
545 | self::expectException(ODMException::class);
546 | $this->itemManager->remove($user);
547 | }
548 |
549 | public function testUnmanagedRefresh()
550 | {
551 | $user = new User();
552 | self::expectException(ODMException::class);
553 | $this->itemManager->refresh($user);
554 | }
555 |
556 | public function testUnmanagedDetach()
557 | {
558 | $user = new User();
559 | self::expectException(ODMException::class);
560 | $this->itemManager->detach($user);
561 | }
562 |
563 | public function testMapAndListData()
564 | {
565 | $game = new ConsoleGame();
566 | $game->setGamecode('ps4koi-' . time());
567 | $game->setFamily('ps4');
568 | $game->setLanguage('en');
569 | $game->setAchievements(
570 | [
571 | "all" => 10,
572 | "hello" => 30,
573 | "deep" => [
574 | "a" => "xyz",
575 | "b" => "jjk",
576 | ],
577 | ]
578 | );
579 | $game->setAuthors(
580 | [
581 | "james",
582 | "curry",
583 | "love",
584 | ]
585 | );
586 | $this->itemManager->persist($game);
587 | $this->itemManager->flush();
588 |
589 | $game->setAuthors(
590 | [
591 | "durant",
592 | "green",
593 | ]
594 | );
595 | $this->itemManager->flush();
596 |
597 | $game->setAchievements(
598 | [
599 | "all" => 10,
600 | "hello" => 30,
601 | "deep" => [
602 | "a" => "xyz",
603 | //"b" => "jjk",
604 | ],
605 | ]
606 | );
607 | $this->itemManager->flush();
608 | }
609 |
610 | public function testProjectedData()
611 | {
612 | $this->itemManager->getRepository(Game::class)->removeAll();
613 |
614 | $game = new Game();
615 | $game->setGamecode('narutofr');
616 | $game->setFamily('naruto');
617 | $game->setLanguage('fr');
618 | $this->itemManager->persist($game);
619 | $this->itemManager->flush();
620 |
621 | /** @var BasicGameInfo $basicInfo */
622 | $basicInfo = $this->itemManager->getRepository(BasicGameInfo::class)->get(['gamecode' => 'narutofr']);
623 | $this->assertTrue($basicInfo instanceof BasicGameInfo);
624 |
625 | $basicInfo->setFamily('helll');
626 | $this->expectException(ODMException::class);
627 | $this->itemManager->flush();
628 | }
629 | }
630 |
--------------------------------------------------------------------------------
/src/ItemRepository.php:
--------------------------------------------------------------------------------
1 | itemManager = $itemManager;
42 | $this->itemReflection = $itemReflection;
43 |
44 | // initialize database connection
45 | $tableName = $itemManager->getDefaultTablePrefix().$itemReflection->getTableName();
46 | $this->dbConnection = $itemManager->createDBConnection();
47 | $this->dbConnection->setTableName($tableName);
48 | $this->dbConnection->setAttributeTypes($itemReflection->getAttributeTypes());
49 | $this->dbConnection->setItemReflection($itemReflection);
50 | }
51 |
52 | public function batchGet($groupOfKeys, $isConsistentRead = false)
53 | {
54 | /** @var string[] $fieldNameMapping */
55 | $fieldNameMapping = $this->itemReflection->getFieldNameMapping();
56 | $groupOfTranslatedKeys = [];
57 | foreach ($groupOfKeys as $keys) {
58 | $translatedKeys = [];
59 | foreach ($keys as $k => $v) {
60 | if (!isset($fieldNameMapping[$k])) {
61 | throw new ODMException("Cannot find primary index field: $k!");
62 | }
63 | $k = $fieldNameMapping[$k];
64 | $translatedKeys[$k] = $v;
65 | }
66 | $groupOfTranslatedKeys[] = $translatedKeys;
67 | }
68 | $resultSet = $this->dbConnection->batchGet(
69 | $groupOfTranslatedKeys,
70 | $isConsistentRead,
71 | 10,
72 | $this->itemReflection->getProjectedAttributes()
73 | );
74 | if (is_array($resultSet)) {
75 | $ret = [];
76 | foreach ($resultSet as $singleResult) {
77 | $obj = $this->persistFetchedItemData($singleResult);
78 | $ret[] = $obj;
79 | }
80 |
81 | return $ret;
82 | }
83 | else {
84 | throw new UnderlyingDatabaseException("Result returned from database for BatchGet() is not an array!");
85 | }
86 | }
87 |
88 | public function clear()
89 | {
90 | $this->itemManaged = [];
91 | }
92 |
93 | public function detach($obj)
94 | {
95 | if (!$this->itemReflection->getReflectionClass()->isInstance($obj)) {
96 | throw new ODMException(
97 | "Object detached is not of correct type, expected: ".$this->itemReflection->getItemClass()
98 | );
99 | }
100 | $id = $this->itemReflection->getPrimaryIdentifier($obj);
101 | if (!isset($this->itemManaged[$id])) {
102 | throw new ODMException("Object is not managed: ".print_r($obj, true));
103 | }
104 |
105 | unset($this->itemManaged[$id]);
106 | }
107 |
108 | public function flush()
109 | {
110 | $skipCAS = $this->itemManager->shouldSkipCheckAndSet()
111 | || (count($this->itemReflection->getCasProperties()) == 0);
112 | $removed = [];
113 | $batchRemovalKeys = [];
114 | $batchSetItems = [];
115 | $batchNewItemStates = new SplStack();
116 | $batchUpdateItemStates = new SplStack();
117 | foreach ($this->itemManaged as $oid => $managedItemState) {
118 | $item = $managedItemState->getItem();
119 | if ($managedItemState->isRemoved()) {
120 | $batchRemovalKeys[] = $this->itemReflection->getPrimaryKeys($item);
121 | $removed[] = $oid;
122 | }
123 | elseif ($managedItemState->isNew()) {
124 | if ($this->itemReflection->getItemDefinition()->projected) {
125 | throw new ODMException(
126 | sprintf(
127 | "Not possible to create a projected item of type %s, try create the full-featured item instead!",
128 | $this->itemReflection->getItemClass()
129 | )
130 | );
131 | }
132 |
133 | $managedItemState->updateCASTimestamps();
134 | $managedItemState->updatePartitionedHashKeys();
135 |
136 | if ($skipCAS) {
137 | $batchSetItems[] = $this->itemReflection->dehydrate($item);
138 | $batchNewItemStates->push($managedItemState);
139 | }
140 | else {
141 | $ret = $this->dbConnection->set(
142 | $this->itemReflection->dehydrate($item),
143 | $managedItemState->getCheckConditionData()
144 | );
145 | if ($ret === false) {
146 | throw new DataConsistencyException(
147 | "Item exists! type = ".$this->itemReflection->getItemClass()
148 | );
149 | }
150 | $managedItemState->setState(ManagedItemState::STATE_MANAGED);
151 | $managedItemState->setUpdated();
152 | }
153 | }
154 | else {
155 | $hasData = $managedItemState->hasDirtyData();
156 | if ($hasData) {
157 | if ($this->itemReflection->getItemDefinition()->projected) {
158 | throw new ODMException(
159 | sprintf(
160 | "Not possible to update a projected item of type %s, try updating the full-featured item instead!"
161 | ." You could also detach the modified item to bypass this exception!",
162 | $this->itemReflection->getItemClass()
163 | )
164 | );
165 | }
166 |
167 | $managedItemState->updateCASTimestamps();
168 | $managedItemState->updatePartitionedHashKeys();
169 | if ($skipCAS) {
170 | $batchSetItems[] = $this->itemReflection->dehydrate($item);
171 | $batchUpdateItemStates->push($managedItemState);
172 | }
173 | else {
174 | $ret = $this->dbConnection->set(
175 | $this->itemReflection->dehydrate($item),
176 | $managedItemState->getCheckConditionData()
177 | );
178 | if (!$ret) {
179 | throw new DataConsistencyException(
180 | "Item updated elsewhere! type = ".$this->itemReflection->getItemClass()
181 | );
182 | }
183 | $managedItemState->setUpdated();
184 | }
185 | }
186 | }
187 | }
188 | if ($batchRemovalKeys) {
189 | $this->dbConnection->batchDelete($batchRemovalKeys);
190 | }
191 | if ($batchSetItems) {
192 | $this->dbConnection->batchPut($batchSetItems);
193 | }
194 | /** @var ManagedItemState $managedItemState */
195 | foreach ($batchNewItemStates as $managedItemState) {
196 | $managedItemState->setState(ManagedItemState::STATE_MANAGED);
197 | $managedItemState->setUpdated();
198 | }
199 | foreach ($batchUpdateItemStates as $managedItemState) {
200 | $managedItemState->setState(ManagedItemState::STATE_MANAGED);
201 | $managedItemState->setUpdated();
202 | }
203 | foreach ($removed as $id) {
204 | unset($this->itemManaged[$id]);
205 | }
206 | }
207 |
208 | public function get($keys, $isConsistentRead = false)
209 | {
210 | /** @var string[] $fieldNameMapping */
211 | $fieldNameMapping = $this->itemReflection->getFieldNameMapping();
212 | $translatedKeys = [];
213 | foreach ($keys as $k => $v) {
214 | if (!isset($fieldNameMapping[$k])) {
215 | throw new ODMException("Cannot find primary index field: $k!");
216 | }
217 | $k = $fieldNameMapping[$k];
218 | $translatedKeys[$k] = $v;
219 | }
220 |
221 | // return existing item
222 | if (!$isConsistentRead) {
223 | $id = $this->itemReflection->getPrimaryIdentifier($translatedKeys);
224 | if (isset($this->itemManaged[$id])) {
225 | return $this->itemManaged[$id]->getItem();
226 | }
227 | }
228 |
229 | $result = $this->dbConnection->get(
230 | $translatedKeys,
231 | $isConsistentRead,
232 | $this->itemReflection->getProjectedAttributes()
233 | );
234 | if (is_array($result)) {
235 | return $this->persistFetchedItemData($result);
236 | }
237 | elseif ($result === null) {
238 | return null;
239 | }
240 | else {
241 | throw new UnderlyingDatabaseException("Result returned from database is not an array!");
242 | }
243 | }
244 |
245 | public function multiQueryAndRun(
246 | callable $callback,
247 | $hashKey,
248 | $hashKeyValues,
249 | $rangeConditions,
250 | array $params,
251 | $indexName,
252 | $filterExpression = '',
253 | $evaluationLimit = 30,
254 | $isConsistentRead = false,
255 | $isAscendingOrder = true,
256 | $concurrency = 10
257 | ) {
258 | if (!is_array($hashKeyValues)) {
259 | $hashKeyValues = [$hashKeyValues];
260 | }
261 | $partitionedHashKeyValues = [];
262 | foreach ($hashKeyValues as $hashKeyValue) {
263 | $partitionedHashKeyValues = array_merge(
264 | $partitionedHashKeyValues,
265 | $this->itemReflection->getAllPartitionedValues($hashKey, $hashKeyValue)
266 | );
267 | }
268 | $fields = array_merge($this->getFieldsArray($rangeConditions), $this->getFieldsArray($filterExpression));
269 | $this->dbConnection->multiQueryAndRun(
270 | function ($result) use ($callback) {
271 | $obj = $this->persistFetchedItemData($result);
272 |
273 | return call_user_func($callback, $obj);
274 | },
275 | $hashKey,
276 | $partitionedHashKeyValues,
277 | $rangeConditions,
278 | $fields,
279 | $params,
280 | $indexName,
281 | $filterExpression,
282 | $evaluationLimit,
283 | $isConsistentRead,
284 | $isAscendingOrder,
285 | $concurrency,
286 | $this->itemReflection->getProjectedAttributes()
287 | );
288 | }
289 |
290 | public function multiQueryCount(
291 | $hashKey,
292 | $hashKeyValues,
293 | $rangeConditions,
294 | array $params,
295 | $indexName,
296 | $filterExpression = '',
297 | $isConsistentRead = false,
298 | $concurrency = 10
299 | ) {
300 | if (!is_array($hashKeyValues)) {
301 | $hashKeyValues = [$hashKeyValues];
302 | }
303 | $partitionedHashKeyValues = [];
304 | foreach ($hashKeyValues as $hashKeyValue) {
305 | $partitionedHashKeyValues = array_merge(
306 | $partitionedHashKeyValues,
307 | $this->itemReflection->getAllPartitionedValues($hashKey, $hashKeyValue)
308 | );
309 | }
310 | $fields = array_merge($this->getFieldsArray($rangeConditions), $this->getFieldsArray($filterExpression));
311 | $count = 0;
312 | $this->dbConnection->multiQueryAndRun(
313 | function () use (&$count) {
314 | $count++;
315 | },
316 | $hashKey,
317 | $partitionedHashKeyValues,
318 | $rangeConditions,
319 | $fields,
320 | $params,
321 | $indexName,
322 | $filterExpression,
323 | 10000,
324 | $isConsistentRead,
325 | true,
326 | $concurrency,
327 | $this->itemReflection->getProjectedAttributes()
328 | );
329 |
330 | return $count;
331 | }
332 |
333 | public function parallelScanAndRun(
334 | $parallel,
335 | callable $callback,
336 | $conditions = '',
337 | array $params = [],
338 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
339 | $isConsistentRead = false,
340 | $isAscendingOrder = true
341 | ) {
342 | $fields = $this->getFieldsArray($conditions);
343 | $this->dbConnection->parallelScanAndRun(
344 | $parallel,
345 | function ($result) use ($callback) {
346 | $obj = $this->persistFetchedItemData($result);
347 |
348 | return call_user_func($callback, $obj);
349 | },
350 | $conditions,
351 | $fields,
352 | $params,
353 | $indexName,
354 | $isConsistentRead,
355 | $isAscendingOrder,
356 | $this->itemReflection->getProjectedAttributes()
357 | );
358 | }
359 |
360 | public function persist($obj)
361 | {
362 | if (!$this->itemReflection->getReflectionClass()->isInstance($obj)) {
363 | throw new ODMException("Persisting wrong object, expecting: ".$this->itemReflection->getItemClass());
364 | }
365 | $id = $this->itemReflection->getPrimaryIdentifier($obj);
366 | if (isset($this->itemManaged[$id])) {
367 | throw new ODMException("Persisting existing object: ".print_r($obj, true));
368 | }
369 |
370 | $managedState = new ManagedItemState($this->itemReflection, $obj);
371 | $managedState->setState(ManagedItemState::STATE_NEW);
372 | $this->itemManaged[$id] = $managedState;
373 | }
374 |
375 | public function query(
376 | $conditions,
377 | array $params,
378 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
379 | $filterExpression = '',
380 | &$lastKey = null,
381 | $evaluationLimit = 30,
382 | $isConsistentRead = false,
383 | $isAscendingOrder = true
384 | ) {
385 | $fields = array_merge($this->getFieldsArray($conditions), $this->getFieldsArray($filterExpression));
386 | $results = $this->dbConnection->query(
387 | $conditions,
388 | $fields,
389 | $params,
390 | $indexName,
391 | $filterExpression,
392 | $lastKey,
393 | $evaluationLimit,
394 | $isConsistentRead,
395 | $isAscendingOrder,
396 | $this->itemReflection->getProjectedAttributes()
397 | );
398 | $ret = [];
399 | foreach ($results as $result) {
400 | $obj = $this->persistFetchedItemData($result);
401 | $ret[] = $obj;
402 | }
403 |
404 | return $ret;
405 | }
406 |
407 | /**
408 | * @param string $conditions
409 | * @param array $params
410 | * @param bool $indexName
411 | * @param string $filterExpression
412 | * @param bool $isConsistentRead
413 | * @param bool $isAscendingOrder
414 | *
415 | * @return SplDoublyLinkedList
416 | *
417 | */
418 | public function queryAll(
419 | $conditions = '',
420 | array $params = [],
421 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
422 | $filterExpression = '',
423 | $isConsistentRead = false,
424 | $isAscendingOrder = true
425 | ) {
426 | $ret = new SplDoublyLinkedList();
427 | $this->queryAndRun(
428 | function ($item) use ($ret) {
429 | $ret->push($item);
430 | },
431 | $conditions,
432 | $params,
433 | $indexName,
434 | $filterExpression,
435 | $isConsistentRead,
436 | $isAscendingOrder
437 | );
438 |
439 | return $ret;
440 | }
441 |
442 | public function queryAndRun(
443 | callable $callback,
444 | $conditions = '',
445 | array $params = [],
446 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
447 | $filterExpression = '',
448 | $isConsistentRead = false,
449 | $isAscendingOrder = true
450 | ) {
451 | $fields = array_merge($this->getFieldsArray($conditions), $this->getFieldsArray($filterExpression));
452 | $this->dbConnection->queryAndRun(
453 | function ($result) use ($callback) {
454 | $obj = $this->persistFetchedItemData($result);
455 |
456 | return call_user_func($callback, $obj);
457 | },
458 | $conditions,
459 | $fields,
460 | $params,
461 | $indexName,
462 | $filterExpression,
463 | $isConsistentRead,
464 | $isAscendingOrder,
465 | $this->itemReflection->getProjectedAttributes()
466 | );
467 | }
468 |
469 | public function queryCount(
470 | $conditions,
471 | array $params,
472 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
473 | $filterExpression = '',
474 | $isConsistentRead = false
475 | ) {
476 | $fields = array_merge($this->getFieldsArray($conditions), $this->getFieldsArray($filterExpression));
477 |
478 | return $this->dbConnection->queryCount(
479 | $conditions,
480 | $fields,
481 | $params,
482 | $indexName,
483 | $filterExpression,
484 | $isConsistentRead
485 | );
486 | }
487 |
488 | public function refresh($obj, $persistIfNotManaged = false)
489 | {
490 | if (!$this->itemReflection->getReflectionClass()->isInstance($obj)) {
491 | throw new ODMException(
492 | "Object refreshed is not of correct type, expected: ".$this->itemReflection->getItemClass()
493 | );
494 | }
495 |
496 | // 2017-03-24: we can refresh something that's not managed
497 | //$id = $this->itemReflection->getPrimaryIdentifier($obj);
498 | //if (!isset($this->itemManaged[$id])) {
499 | // throw new ODMException("Object is not managed: " . print_r($obj, true));
500 | //}
501 | // end of change 2017-03-24
502 |
503 | $id = $this->itemReflection->getPrimaryIdentifier($obj);
504 | if (!isset($this->itemManaged[$id])) {
505 | if ($persistIfNotManaged) {
506 | $this->itemManaged[$id] = new ManagedItemState($this->itemReflection, $obj);
507 | }
508 | else {
509 | throw new ODMException("Object is not managed: ".print_r($obj, true));
510 | }
511 | }
512 |
513 | $objRefreshed = $this->get($this->itemReflection->getPrimaryKeys($obj, false), true);
514 |
515 | if (!$objRefreshed && $persistIfNotManaged) {
516 | $this->itemManaged[$id]->setState(ManagedItemState::STATE_NEW);
517 | }
518 | }
519 |
520 | public function remove($obj)
521 | {
522 | if (!$this->itemReflection->getReflectionClass()->isInstance($obj)) {
523 | throw new ODMException(
524 | "Object removed is not of correct type, expected: ".$this->itemReflection->getItemClass()
525 | );
526 | }
527 | $id = $this->itemReflection->getPrimaryIdentifier($obj);
528 | if (!isset($this->itemManaged[$id])) {
529 | throw new ODMException("Object is not managed: ".print_r($obj, true));
530 | }
531 |
532 | $this->itemManaged[$id]->setState(ManagedItemState::STATE_REMOVED);
533 | }
534 |
535 | public function removeAll()
536 | {
537 | do {
538 | $this->clear();
539 | $this->scanAndRun(
540 | function ($item) {
541 | $this->remove($item);
542 | if (count($this->itemManaged) > 1000) {
543 | return false;
544 | }
545 |
546 | return true;
547 | },
548 | '',
549 | [],
550 | DynamoDbIndex::PRIMARY_INDEX,
551 | true,
552 | true,
553 | 10
554 | );
555 | if (count($this->itemManaged) == 0) {
556 | break;
557 | }
558 | $skipCAS = $this->itemManager->shouldSkipCheckAndSet();
559 | $this->itemManager->setSkipCheckAndSet(true);
560 | $this->flush();
561 | $this->itemManager->setSkipCheckAndSet($skipCAS);
562 | } while (true);
563 | }
564 |
565 | public function removeById($keys)
566 | {
567 | $obj = $this->get($keys, true);
568 | if ($obj) {
569 | $this->remove($obj);
570 | }
571 | }
572 |
573 | public function scan(
574 | $conditions = '',
575 | array $params = [],
576 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
577 | &$lastKey = null,
578 | $evaluationLimit = 30,
579 | $isConsistentRead = false,
580 | $isAscendingOrder = true
581 | ) {
582 | $fields = $this->getFieldsArray($conditions);
583 | $results = $this->dbConnection->scan(
584 | $conditions,
585 | $fields,
586 | $params,
587 | $indexName,
588 | $lastKey,
589 | $evaluationLimit,
590 | $isConsistentRead,
591 | $isAscendingOrder,
592 | $this->itemReflection->getProjectedAttributes()
593 | );
594 | $ret = [];
595 | foreach ($results as $result) {
596 | $obj = $this->persistFetchedItemData($result);
597 | $ret[] = $obj;
598 | }
599 |
600 | return $ret;
601 | }
602 |
603 | /**
604 | * @param string $conditions
605 | * @param array $params
606 | * @param bool $indexName
607 | * @param bool $isConsistentRead
608 | * @param bool $isAscendingOrder
609 | * @param int $parallel
610 | *
611 | * @return SplDoublyLinkedList
612 | */
613 | public function scanAll(
614 | $conditions = '',
615 | array $params = [],
616 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
617 | $isConsistentRead = false,
618 | $isAscendingOrder = true,
619 | $parallel = 1
620 | ) {
621 | $ret = new SplDoublyLinkedList();
622 | $this->scanAndRun(
623 | function ($item) use ($ret) {
624 | $ret->push($item);
625 | },
626 | $conditions,
627 | $params,
628 | $indexName,
629 | $isConsistentRead,
630 | $isAscendingOrder,
631 | $parallel
632 | );
633 |
634 | return $ret;
635 | }
636 |
637 | public function scanAndRun(
638 | callable $callback,
639 | $conditions = '',
640 | array $params = [],
641 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
642 | $isConsistentRead = false,
643 | $isAscendingOrder = true,
644 | $parallel = 1
645 | ) {
646 | $resultCallback = function ($result) use ($callback) {
647 | $obj = $this->persistFetchedItemData($result);
648 |
649 | return call_user_func($callback, $obj);
650 | };
651 |
652 | $fields = $this->getFieldsArray($conditions);
653 |
654 | if ($parallel > 1) {
655 | $this->dbConnection->parallelScanAndRun(
656 | $parallel,
657 | $resultCallback,
658 | $conditions,
659 | $fields,
660 | $params,
661 | $indexName,
662 | $isConsistentRead,
663 | $isAscendingOrder,
664 | $this->itemReflection->getProjectedAttributes()
665 | );
666 | }
667 | elseif ($parallel == 1) {
668 | $this->dbConnection->scanAndRun(
669 | $resultCallback,
670 | $conditions,
671 | $fields,
672 | $params,
673 | $indexName,
674 | $isConsistentRead,
675 | $isAscendingOrder,
676 | $this->itemReflection->getProjectedAttributes()
677 | );
678 | }
679 | else {
680 | throw new InvalidArgumentException("Parallel can only be an integer greater than 0");
681 | }
682 | }
683 |
684 | public function scanCount(
685 | $conditions = '',
686 | array $params = [],
687 | $indexName = DynamoDbIndex::PRIMARY_INDEX,
688 | $isConsistentRead = false,
689 | $parallel = 10
690 | ) {
691 | $fields = $this->getFieldsArray($conditions);
692 |
693 | return $this->dbConnection->scanCount(
694 | $conditions,
695 | $fields,
696 | $params,
697 | $indexName,
698 | $isConsistentRead,
699 | $parallel
700 | );
701 | }
702 |
703 | /**
704 | * @return Connection
705 | *
706 | *
707 | * @deprecated this interface might be removed any time in the future
708 | *
709 | * @internal only for advanced user, avoid using the table client directly whenever possible.
710 | */
711 | public function getDynamodbTable()
712 | {
713 | return $this->dbConnection;
714 | }
715 |
716 | protected function getFieldsArray($conditions)
717 | {
718 | $ret = preg_match_all('/#(?P[a-zA-Z_][a-zA-Z0-9_]*)/', $conditions, $matches);
719 | if (!$ret) {
720 | return [];
721 | }
722 |
723 | $result = [];
724 | $fieldNameMapping = $this->itemReflection->getFieldNameMapping();
725 | if (isset($matches['field']) && is_array($matches['field'])) {
726 | foreach ($matches['field'] as $fieldName) {
727 | if (!isset($fieldNameMapping[$fieldName])) {
728 | throw new ODMException("Cannot find field named $fieldName!");
729 | }
730 | $result["#".$fieldName] = $fieldNameMapping[$fieldName];
731 | }
732 | }
733 |
734 | return $result;
735 | }
736 |
737 | protected function persistFetchedItemData(array $resultData)
738 | {
739 | $id = $this->itemReflection->getPrimaryIdentifier($resultData);
740 | if (isset($this->itemManaged[$id])) {
741 | if ($this->itemManaged[$id]->isNew()) {
742 | throw new ODMException("Conflict! Fetched remote data is also persisted. ".json_encode($resultData));
743 | }
744 | elseif ($this->itemManaged[$id]->isRemoved()) {
745 | throw new ODMException("Conflict! Fetched remote data is also removed. ".json_encode($resultData));
746 | }
747 |
748 | $obj = $this->itemManaged[$id]->getItem();
749 | $this->itemReflection->hydrate($resultData, $obj);
750 | $this->itemManaged[$id]->setOriginalData($resultData);
751 | }
752 | else {
753 | $obj = $this->itemReflection->hydrate($resultData);
754 | $this->itemManaged[$id] = new ManagedItemState($this->itemReflection, $obj, $resultData);
755 | }
756 |
757 | return $obj;
758 | }
759 | }
760 |
--------------------------------------------------------------------------------