├── ActiveQuery.php ├── ActiveRecord.php ├── BadCountResultException.php ├── Client.php ├── ConditionBuilder.php ├── Exception.php ├── LICENSE ├── LongRequestsException.php ├── Profiler.php ├── README.md ├── Schema.php ├── composer.json ├── docs └── models │ ├── CheckKkm.php │ └── CheckKkmPayment.php └── logger ├── Container.php ├── Profiler.php └── RequestsMonitor.php /ActiveQuery.php: -------------------------------------------------------------------------------- 1 | emulateExecution) { 35 | return false; 36 | } 37 | 38 | $this->limit(1); 39 | $data = $this->getData(); 40 | if ($data) { 41 | $models = $this->populate($data); 42 | return reset($models) ?: null; 43 | } else { 44 | return null; 45 | } 46 | } 47 | 48 | /** 49 | * Executes the query and returns all results as an array. 50 | * @param Connection $db the database connection used to generate the SQL statement. 51 | * If this parameter is not given, the `db` application component will be used. 52 | * @return array the query results. If the query results in nothing, an empty array will be returned. 53 | */ 54 | public function all($db = null) 55 | { 56 | if ($this->emulateExecution) { 57 | return []; 58 | } 59 | 60 | $rows = $this->getData(); 61 | return $this->populate($rows); 62 | } 63 | 64 | public function count($q = '*', $db = null) { 65 | $table = $this->getTableName(); 66 | $filters = $this->getFilters(); 67 | $client = $this->getClient(); 68 | $count = 0; 69 | foreach ($filters as $filter) { 70 | $subCount = $client->{$table . '/$count'}->get(null, $filter, $this->getOptions()); 71 | if (!is_int($subCount) && !is_string($subCount)) { 72 | throw new BadCountResultException('Bad returned count result: ' . var_export($subCount, true)); 73 | } 74 | 75 | 76 | $count += $subCount; 77 | if (!$count) { 78 | break; 79 | } 80 | } 81 | 82 | if (!empty($count)) { 83 | return $count; 84 | } 85 | } 86 | 87 | protected function getFilters() { 88 | if ($this->primaryModel !== null) { 89 | $idKey = key($this->link); 90 | $primaryKey = current($this->link); 91 | if ($this->via) { 92 | $viaIdKey = key($this->via[1]->link); 93 | $viaPrimaryKey = current($this->via[1]->link); 94 | $viaQuery = $this->via[1]; 95 | $this->andWhere([ 96 | $idKey => $viaQuery->select($primaryKey), 97 | ]); 98 | } else { 99 | $id = $this->primaryModel->$primaryKey; 100 | if (empty($id)) { 101 | return [self::EMPTY_CONDITION_STUB]; 102 | } 103 | 104 | $this->andWhere([ 105 | $idKey => $id 106 | ]); 107 | } 108 | } 109 | 110 | $where = $this->where; 111 | 112 | if (is_array($where)) { 113 | $where = $this->filterCondition($where); 114 | } 115 | 116 | if ($where === null) { 117 | return ['true eq true']; 118 | } 119 | 120 | $filters = $this->buildFilters($where); 121 | 122 | return $filters; 123 | } 124 | 125 | public function getClient() { 126 | $modelClass = $this->modelClass; 127 | 128 | return $modelClass::getClient(); 129 | } 130 | 131 | /** 132 | * @param $client 133 | */ 134 | protected function getData() 135 | { 136 | if (!empty($this->primaryModel)) { 137 | $idKey = key($this->link); 138 | $primaryKey = current($this->link); 139 | if ($this->via) { 140 | $viaIdKey = key($this->via[1]->link); 141 | $viaPrimaryKey = current($this->via[1]->link); 142 | $viaQuery = $this->via[1]; 143 | $this->andWhere([ 144 | $idKey => $viaQuery->select($primaryKey), 145 | ]); 146 | } else { 147 | $pk = $this->primaryModel->$primaryKey; 148 | $this->andWhere([ 149 | $idKey => $pk 150 | ]); 151 | } 152 | } 153 | 154 | $tableName = $this->getTableName(); 155 | $options = $this->getOptions(); 156 | $client = $this->getClient(); 157 | $filters = $this->getFilters(); 158 | $result = []; 159 | foreach ($filters as $filter) { 160 | $data = $client->$tableName->get(null, $filter, $options); 161 | if (!empty($data['value'])) { 162 | $result = array_merge($result, $data['value']); 163 | } 164 | } 165 | 166 | return $result; 167 | } 168 | 169 | protected function getOptions() { 170 | $query = []; 171 | if ($this->offset !== -1) { 172 | $query['$skip'] = $this->offset; 173 | } 174 | 175 | if ($this->limit !== -1) { 176 | $query['$top'] = $this->limit; 177 | } else { 178 | // $query['$top'] = 1; 179 | // throw new Exception('Query without limit'); 180 | } 181 | 182 | // $query['$top'] = 1; 183 | 184 | if (!empty($this->select)) { 185 | $query['$select'] = implode(',', $this->select); 186 | } 187 | 188 | if (!empty($this->orderBy)) { 189 | $query['$orderby'] = $this->buildOrder(); 190 | } 191 | 192 | return [ 193 | 'query' => $query, 194 | ]; 195 | } 196 | 197 | protected function buildOrder() { 198 | $orderParts = []; 199 | foreach ($this->orderBy as $attribute => $direction) { 200 | if (is_int($attribute)) { 201 | $orderParts[] = $direction; 202 | continue; 203 | } 204 | 205 | $attribute = trim($attribute, '\''); 206 | $attribute = '\'' . $attribute . '\''; 207 | if ($direction === SORT_ASC) { 208 | $direction = 'asc'; 209 | } else { 210 | $direction = 'desc'; 211 | } 212 | 213 | $orderParts[] = $attribute . ' ' . $direction; 214 | } 215 | return implode(',', $orderParts); 216 | } 217 | 218 | public function exists($db = null) 219 | { 220 | return $this->count() > 0; 221 | } 222 | 223 | /** 224 | * @return mixed 225 | */ 226 | protected function getTableName() 227 | { 228 | $class = $this->modelClass; 229 | $tableName = $class::tableName(); 230 | return $tableName; 231 | } 232 | 233 | const MAX_WHERE_LENGTH = 3000; 234 | protected function deleteDublicatesFromWhere($where) { 235 | $existed = []; 236 | foreach ($where as $whereKey => $item) { 237 | $key = serialize($item); 238 | if (isset($existed[$key])) { 239 | unset($where[$whereKey]); 240 | } else { 241 | $existed[$key] = true; 242 | } 243 | } 244 | 245 | return $where; 246 | } 247 | 248 | protected function buildFilters($where) { 249 | $where = $this->parseWhere($where); 250 | $where = array_filter($where); 251 | $where = $this->deleteDublicatesFromWhere($where); 252 | 253 | if (empty($where)) { 254 | return []; 255 | } 256 | $result = []; 257 | $filterPrefx = ''; 258 | $arrayValue = null; 259 | foreach ($where as &$value) { 260 | if (is_array($value)) { 261 | if ($arrayValue !== null) { 262 | throw new Exception('Many in where conditions is not supported'); 263 | } 264 | 265 | $arrayValue = $value; 266 | } else { 267 | $result[] = $value; 268 | } 269 | } 270 | 271 | if (!empty($result)) { 272 | $filterPrefx = '(' . implode(' and ', $result) . ')'; 273 | if ($arrayValue !== null) { 274 | $filterPrefx .= ' and '; 275 | } 276 | } else { 277 | $filterPrefx = ''; 278 | } 279 | 280 | if ($arrayValue === null) { 281 | if (!empty($filterPrefx)) { 282 | return [$filterPrefx]; 283 | } else { 284 | return []; 285 | } 286 | } 287 | 288 | $result = []; 289 | $currentFilterPostfix = ''; 290 | foreach ($arrayValue as $key => $value) { 291 | if ($currentFilterPostfix === '') { 292 | $currentFilterPostfix = $value; 293 | } else { 294 | $currentFilterPostfix .= ' or ' . $value; 295 | } 296 | 297 | $filter = $filterPrefx . '(' . $currentFilterPostfix . ')'; 298 | if (strlen($filter) > self::MAX_WHERE_LENGTH) { 299 | throw new Exception('Very big condition. Make it smaller: ' . $filter); 300 | } 301 | 302 | if ($key + 1 == count($arrayValue) || strlen($filterPrefx . '(' . $currentFilterPostfix . ' or ' . $arrayValue[$key + 1]) > self::MAX_WHERE_LENGTH) { 303 | if ($currentFilterPostfix) { 304 | $r = $filterPrefx . '(' . $currentFilterPostfix . ')'; 305 | } 306 | 307 | $result[] = $r; 308 | $currentFilterPostfix = ''; 309 | } 310 | } 311 | 312 | return $result; 313 | } 314 | 315 | /** 316 | * @param $where 317 | * @return string 318 | */ 319 | protected function parseWhere($where, $condition = 'and') 320 | { 321 | if (is_string($where)) { 322 | return [$where]; 323 | } 324 | 325 | $searchQueries = []; 326 | if (isset($where[0])) { 327 | $condition = $where[0]; 328 | unset($where[0]); 329 | if (strtolower($condition) === 'ilike' || strtolower($condition) === 'like') { 330 | if (!empty($where[1]) && !empty($where[2]) && is_string($where[1]) && is_string($where[2])) { 331 | $searchQueries[] = $this->buildColumnCondition($where[1], '%' . $where[2] . '%', 'like'); 332 | } 333 | } else if (strtolower($condition) === 'in') { 334 | if (!empty($where[1]) && !empty($where[2])) { 335 | if (is_array($where[1])) { 336 | if (count($where[1]) !== 1) { 337 | throw new Exception('Many attributes of IN condition is not supported'); 338 | } 339 | 340 | $where[1] = current($where[1]); 341 | } 342 | 343 | if (!(is_string($where[1]) && is_array($where[2]))) { 344 | throw new Exception('This variation of in condition is not supported'); 345 | } 346 | 347 | if ($where[2] instanceof \yii\db\ActiveQuery) { 348 | $ids = $where[2]->column(); 349 | $ids = array_filter($ids); 350 | $where[2] = $ids; 351 | } 352 | 353 | if (is_array($where[2])) { 354 | $subWhere = []; 355 | foreach ($where[2] as $v) { 356 | $subWhere[] = $this->buildColumnCondition($where[1], $v); 357 | } 358 | 359 | $searchQueries[] = $subWhere; 360 | } else { 361 | $searchQueries[] = $this->buildColumnCondition($where[1], $where[2]); 362 | } 363 | } 364 | } else { 365 | foreach ($where as $value) { 366 | $subWhere = $this->parseWhere($value, $condition); 367 | foreach ($subWhere as $where) { 368 | $searchQueries[] = $where; 369 | } 370 | } 371 | } 372 | } else { 373 | foreach ($where as $attribute => $value) { 374 | if ($value instanceof \yii\db\ActiveQuery) { 375 | $value = $value->column(); 376 | $value = array_filter($value); 377 | } 378 | 379 | if (is_array($value)) { 380 | $subWhere = []; 381 | if (empty($value)) { 382 | $subWhere[] = self::EMPTY_CONDITION_STUB; 383 | } else { 384 | foreach ($value as $v) { 385 | $subWhere[] = $this->buildColumnCondition($attribute, $v); 386 | } 387 | } 388 | 389 | $searchQueries[] = $subWhere; 390 | } else { 391 | $searchQueries[] = $this->buildColumnCondition($attribute, $value); 392 | } 393 | } 394 | } 395 | 396 | return $searchQueries; 397 | } 398 | 399 | protected function buildColumnCondition($column, $value, $operator = null) { 400 | $class = $this->modelClass; 401 | $builder = new ConditionBuilder([ 402 | 'tableSchema' => $class::getTableSchema() 403 | ]); 404 | return $builder->buildColumnCondition($column, $value, $operator); 405 | } 406 | 407 | /** 408 | * Executes the query and returns the first column of the result. 409 | * @param Connection $db the database connection used to generate the SQL statement. 410 | * If this parameter is not given, the `db` application component will be used. 411 | * @return array the first column of the query result. An empty array is returned if the query results in nothing. 412 | */ 413 | public function column($db = null) 414 | { 415 | if ($this->emulateExecution) { 416 | return []; 417 | } 418 | 419 | $rows = $this->getData(); 420 | $results = []; 421 | if (count($this->select) !== 1) { 422 | throw new Exception('Select column with array of columns impossible. Select list: ' . var_export($this->select, true)); 423 | } 424 | 425 | $selectedColumn = current($this->select); 426 | foreach ($rows as $row) { 427 | if (!array_key_exists($selectedColumn, $row)) { 428 | throw new Exception('Failed to get column from data. Data: ' . var_export($rows, true) . '. Row: ' . var_export($row, true)); 429 | } 430 | 431 | $value = $row[$selectedColumn]; 432 | 433 | if ($this->indexBy instanceof \Closure) { 434 | $results[call_user_func($this->indexBy, $row)] = $value; 435 | } else { 436 | $results[] = $value; 437 | } 438 | } 439 | 440 | return $results; 441 | } 442 | 443 | /** 444 | * Executes the query and returns the first column of the result. 445 | * @param Connection $db the database connection used to generate the SQL statement. 446 | * If this parameter is not given, the `db` application component will be used. 447 | * @return array the first column of the query result. An empty array is returned if the query results in nothing. 448 | */ 449 | public function scalar($db = null) 450 | { 451 | return current($this->column()); 452 | } 453 | } -------------------------------------------------------------------------------- /ActiveRecord.php: -------------------------------------------------------------------------------- 1 | getPrimaryKey(true) as $key => $value) { 46 | $pk[$key] = $value; 47 | } 48 | /* @var $record BaseActiveRecord */ 49 | $record = static::findOne($pk); 50 | return $this->refreshInternal($record); 51 | } 52 | 53 | public static function updateAll($attributes, $condition = NULL, $params = []) { 54 | // if ($condition === null || count($condition) != 1 || empty($condition['Ref_Key'])) { 55 | // throw new \yii\base\Exception('updateAll allowed only update by Ref_Key'); 56 | // } 57 | // 58 | // $id = $condition['Ref_Key']; 59 | $client = self::getClient(); 60 | $attributes = static::filtrateAttributes($attributes); 61 | $result = self::doTries(function () use ($client, $attributes, $condition) { 62 | if (empty($condition['Ref_Key'])) { 63 | $client->{static::tableName() . self::buildIdFilter($condition)}->delete(null); 64 | return $client->{static::tableName()}->create(ArrayHelper::merge($attributes, $condition)); 65 | } else { 66 | $id = $condition['Ref_Key']; 67 | return $client->{static::tableName()}->update($id, $attributes); 68 | } 69 | }); 70 | 71 | return $result; 72 | } 73 | 74 | public static function buildFilterByCondition($condition) { 75 | $filterParts = []; 76 | foreach ($condition as $attribute => $value) { 77 | $builder = new ConditionBuilder([ 78 | 'tableSchema' => static::getTableSchema(), 79 | ]); 80 | 81 | $filterParts[] = $builder->buildColumnCondition($attribute, $value); 82 | } 83 | 84 | return implode(' and ', $filterParts); 85 | } 86 | 87 | public static function buildIdFilter($condition) { 88 | $result = []; 89 | foreach ($condition as $attribute => $value) { 90 | $result[] = $attribute . '=\'' . $value . '\''; 91 | } 92 | 93 | return '(' . implode(', ', $result) . ')'; 94 | } 95 | 96 | public static function deleteAll($condition = null, $params = []) { 97 | $client = self::getClient(); 98 | 99 | self::doTries(function () use ($client, $condition) { 100 | if (empty($condition['Ref_Key'])) { 101 | $client->{static::tableName() . self::buildIdFilter($condition)}->delete(null); 102 | } else { 103 | $id = $condition['Ref_Key']; 104 | $client->{static::tableName()}->update($id, [ 105 | 'DeletionMark' => true, 106 | ]); 107 | } 108 | }); 109 | 110 | return true; 111 | } 112 | 113 | /** 114 | * Inserts an ActiveRecord into DB without considering transaction. 115 | * @param array $attributes list of attributes that need to be saved. Defaults to `null`, 116 | * meaning all attributes that are loaded from DB will be saved. 117 | * @return bool whether the record is inserted successfully. 118 | */ 119 | protected function insertInternal($attributes = null) 120 | { 121 | if (!$this->beforeSave(true)) { 122 | return false; 123 | } 124 | $values = $this->getDirtyAttributes($attributes); 125 | $insertResult = $this->doOperation('create'); 126 | unset($insertResult['odata.metadata']); 127 | if (empty($insertResult)) { 128 | return false; 129 | } 130 | foreach ($insertResult as $name => $value) { 131 | if (empty(static::getTableSchema()->columns[$name])) { 132 | continue; 133 | } 134 | 135 | $id = static::getTableSchema()->columns[$name]->phpTypecast($value); 136 | $this->setAttribute($name, $id); 137 | $values[$name] = $id; 138 | } 139 | 140 | $changedAttributes = array_fill_keys(array_keys($values), null); 141 | $this->setOldAttributes($values); 142 | $this->afterSave(true, $changedAttributes); 143 | 144 | return true; 145 | } 146 | 147 | protected static function isGuid($attribute) { 148 | return self::getTableSchema()->getColumn($attribute)->dbType === 'Edm.Guid'; 149 | } 150 | 151 | public static function primaryKey() 152 | { 153 | return self::getTableSchema()->primaryKey; 154 | } 155 | 156 | public static function getDb() 157 | { 158 | // throw new Exception(__FUNCTION__ . ' is not implemented'); 159 | } 160 | 161 | // public static function instantiate($row) { 162 | // var_dump($row); 163 | // exit; 164 | // } 165 | 166 | public static function populateRecord($record, $row) 167 | { 168 | foreach ($row as $key => $value) { 169 | if ($value === '00000000-0000-0000-0000-000000000000') { 170 | unset($row[$key]); 171 | } 172 | } 173 | 174 | return parent::populateRecord($record, $row); // TODO: Change the autogenerated stub 175 | } 176 | 177 | public static function getClient() { 178 | return \yii::$app->oData; 179 | } 180 | 181 | public static function getTableSchema() 182 | { 183 | $oDataSchema = new Schema([ 184 | 'client' => self::getClient(), 185 | ]); 186 | 187 | return $oDataSchema->getTableSchema(static::tableName()); // TODO: Change the autogenerated stub 188 | } 189 | 190 | /** 191 | * @param $value 192 | * @return array 193 | */ 194 | protected static function filtrateAttributes($attributes): array 195 | { 196 | foreach ($attributes as $attribute => &$value) { 197 | if (empty($value) && self::isGuid($attribute)) { 198 | // unset($attributes[$attribute]); 199 | $value = '00000000-0000-0000-0000-000000000000'; 200 | } else if (self::getTableSchema()->getColumn($attribute)->dbType === 'Edm.Boolean') { 201 | if ($value === null) { 202 | unset($attributes[$attribute]); 203 | } else { 204 | $value = (boolean) $value; 205 | } 206 | } 207 | } 208 | // unset($attributes[current(self::primaryKey())]); 209 | unset($attributes['DataVersion']); 210 | // unset($attributes['Code']); 211 | return $attributes; 212 | } 213 | 214 | /** 215 | * @param $operation 216 | * @return bool 217 | * @throws \yii\base\Exception 218 | */ 219 | protected function doOperation($operation) 220 | { 221 | $client = self::getClient(); 222 | $attributes = static::filtrateAttributes($this->attributes); 223 | foreach ($this->complexRelations as $relation) { 224 | $models = $this->$relation; 225 | foreach ($models as $key => $model) { 226 | if (!is_array($model)) { 227 | $models[$key] = $model->attributes; 228 | } 229 | } 230 | 231 | $attributes[$relation] = $models; 232 | $this->$relation = []; 233 | } 234 | 235 | $try = function () use ($client, $operation, $attributes) { 236 | $client->{static::tableName()}; 237 | if ($this->isNewRecord) { 238 | return $client->$operation($attributes); 239 | } else { 240 | return $client->$operation($this->primaryKey, $attributes); 241 | } 242 | }; 243 | $result = $this->doTries($try); 244 | 245 | return $result; 246 | } 247 | 248 | /** 249 | * @param $try 250 | * @return bool 251 | * @throws \execut\oData\Exception 252 | */ 253 | protected static function doTries($try) { 254 | $key = 0; 255 | do { 256 | try { 257 | return $try(); 258 | } catch (\execut\oData\Exception $e) { 259 | if (strpos($e->getMessage(), 'ПриЗаписи') !== false) { 260 | \yii::error('Ошибка обработчика при записи ' . $e->getMessage()); 261 | if ($key === 2) { 262 | throw $e; 263 | } 264 | 265 | $key++; 266 | sleep(1); 267 | continue; 268 | } 269 | 270 | throw $e; 271 | } 272 | } while ($key < 3); 273 | } 274 | 275 | public function doRequest($request) { 276 | $client = self::getClient(); 277 | $client->{static::tableName() . '(guid\'' . $this->primaryKey . '\')/' . $request}; 278 | return $client->request('POST'); 279 | } 280 | 281 | protected static function filterCondition(array $condition, array $aliases = []) 282 | { 283 | return $condition; 284 | } 285 | } -------------------------------------------------------------------------------- /BadCountResultException.php: -------------------------------------------------------------------------------- 1 | cache = []; 31 | } 32 | 33 | public function init() 34 | { 35 | parent::init(); // TODO: Change the autogenerated stub 36 | } 37 | 38 | public function setLogger($logger) { 39 | if (is_array($logger)) { 40 | $logger = \yii::createObject($logger); 41 | } 42 | 43 | $this->_logger = $logger; 44 | } 45 | 46 | public function getLogger() { 47 | return $this->_logger; 48 | } 49 | 50 | public function __call($name, $params) 51 | { 52 | if (!empty($params[1]) && $params[1] === ActiveQuery::EMPTY_CONDITION_STUB) { 53 | $this->getClient()->reset(); 54 | return []; 55 | } 56 | 57 | if ($this->isConnectionError()) { 58 | if ($this->e) { 59 | $message = $this->e->getMessage(); 60 | } else { 61 | $message = ''; 62 | } 63 | 64 | $this->getClient()->reset(); 65 | 66 | throw new Exception($message); 67 | } 68 | 69 | try { 70 | if ($name === 'get') { 71 | $cacheKey = $name . $this->_gettedTable . serialize($params); 72 | if ($result = $this->getCache($cacheKey)) { 73 | $this->getClient()->reset(); 74 | 75 | return $result; 76 | } 77 | } else { 78 | $this->cache = []; 79 | } 80 | 81 | $result = call_user_func_array([$this->getClient(), $name], $params); 82 | if ($name === 'get') { 83 | $this->setCache($cacheKey, $result); 84 | } 85 | } catch (\Exception $e) { 86 | $this->getClient()->reset(); 87 | $this->setIsConnectionError(true); 88 | $this->e = $e; 89 | $message = $e->getMessage() 90 | . '. OData query "' . $name . '" params "' . var_export($params, true) 91 | . '". Trace: ' . $e->getTraceAsString(); 92 | if (strpos($e->getMessage(), 'Ошибка при разборе опции запроса')) { 93 | $message .= '. ' . var_export($params[1], true); 94 | } 95 | 96 | throw new Exception($message, $e->getCode(), $e); 97 | } 98 | 99 | return $result; 100 | } 101 | 102 | public function getCache($key) { 103 | if (!empty($this->cache[$key])) { 104 | return $this->cache[$key]; 105 | } 106 | } 107 | 108 | public function setCache($key, $value) { 109 | $this->cache[$key] = $value; 110 | 111 | return $this; 112 | } 113 | 114 | protected $_gettedTable = null; 115 | public function __get($name) 116 | { 117 | $this->_gettedTable = $name; 118 | $this->getClient()->$name; 119 | 120 | return $this; 121 | } 122 | 123 | protected $isConnectionError = []; 124 | public function isConnectionError() { 125 | if (array_key_exists($this->currentHost, $this->isConnectionError)) { 126 | return $this->isConnectionError[$this->currentHost]; 127 | } 128 | $cacheValue = \yii::$app->cache->get($this->getCacheKey()); 129 | if ($cacheValue) { 130 | try { 131 | $this->getClient()->reset(); 132 | $this->getClient()->Catalog_Контрагенты->get(null, null, [ 133 | 'query' => [ 134 | '$top' => 1, 135 | ] 136 | ]); 137 | } catch (\Exception $e) { 138 | $this->getClient()->reset(); 139 | $this->e = $e; 140 | return $this->isConnectionError[$this->currentHost] = true; 141 | } 142 | 143 | \yii::$app->cache->delete($this->getCacheKey()); 144 | } 145 | 146 | return $this->isConnectionError[$this->currentHost] = false; 147 | } 148 | 149 | protected function getCacheKey() { 150 | return __CLASS__ . '-' . $this->currentHost; 151 | } 152 | 153 | public function setIsConnectionError($value) { 154 | if ($value) { 155 | \yii::$app->cache->set($this->getCacheKey(), true); 156 | } 157 | } 158 | 159 | /** 160 | * @return \Kily\Tools1C\OData\Client 161 | */ 162 | protected function getClient() 163 | { 164 | if ($this->_client === null) { 165 | $this->_client = new \Kily\Tools1C\OData\Client(trim($this->host, '/') . '/' . $this->path, $this->options, $this->getLogger()); 166 | } 167 | 168 | return $this->_client; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /ConditionBuilder.php: -------------------------------------------------------------------------------- 1 | detectOperatorByColumnType($column); 25 | } 26 | 27 | $value = $this->escapeValue($value); 28 | 29 | switch ($operator) { 30 | case 'substringof': 31 | return 'substringof(\'' . $value . '\', ' . $column . ')'; 32 | break; 33 | case 'like': 34 | return 'like(' . $column . ', \'' . $value . '\')'; 35 | break; 36 | case 'eqGuid': 37 | if ($this->isEmptyGuid($value)) { 38 | return; 39 | } 40 | 41 | return $column . ' eq guid\'' . $value . '\''; 42 | break; 43 | case 'eq': 44 | return '\'' . $value . '\' eq ' . $column . ''; 45 | break; 46 | case 'bool': 47 | $value = (bool) $value; 48 | if ($value) { 49 | $value = 'true'; 50 | } else { 51 | $value = 'false'; 52 | } 53 | 54 | return $column . ' eq ' . $value; 55 | break; 56 | case 'castToGuid': 57 | $type = $this->getColumnType($column); 58 | 59 | return $column . ' eq cast(guid\'' . $value . '\', \'' . $type . '\')'; 60 | } 61 | } 62 | 63 | protected function getColumnType($name) { 64 | if ($name === 'Заказы/Заказ_Key') { 65 | return 'Edm.Guid'; 66 | } 67 | $column = $this->tableSchema->getColumn($name); 68 | if (!$column) { 69 | throw new Exception('Column "' . $name . '" is not found'); 70 | } 71 | 72 | return $column->dbType; 73 | } 74 | 75 | public function escapeValue($value) { 76 | return str_replace(['\'', '%27', "\n", "\r", "\0"], '', $value); 77 | } 78 | 79 | public function detectOperatorByColumnType($column) { 80 | $type = $this->getColumnType($column); 81 | switch ($type) { 82 | case 'text2': 83 | return 'substringof'; 84 | break; 85 | case 'text': 86 | return 'like'; 87 | break; 88 | case 'Edm.Guid': 89 | return 'eqGuid'; 90 | break; 91 | case 'Edm.String': 92 | case 'Edm.Int64': 93 | case 'Edm.Double': 94 | case 'Edm.Int': 95 | return 'eq'; 96 | break; 97 | case 'Edm.Boolean': 98 | return 'bool'; 99 | break; 100 | default: 101 | return 'castToGuid'; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /Exception.php: -------------------------------------------------------------------------------- 1 | getMessage(), 'oData'); 19 | } 20 | 21 | public function end() 22 | { 23 | \yii::endProfile($this->getMessage(), 'oData'); 24 | } 25 | 26 | protected function getMessage() { 27 | return trim($this->request->getHost(), '/') . $this->request->getUrl() . ' (' . $this->request->getMethod() . '), options: ' . var_export($this->request->getOptions(), true); 28 | } 29 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yii2 wrapper via activeRecord for 1C oData 2 | ## Installation 3 | 4 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 5 | 6 | ### Install 7 | 8 | Either run 9 | 10 | ``` 11 | $ php composer.phar require execut/yii2-1c-odata "dev-master" 12 | ``` 13 | 14 | or add 15 | 16 | ``` 17 | "execut/yii2-1c-odata": "dev-master" 18 | ``` 19 | 20 | to the ```require``` section of your `composer.json` file. 21 | 22 | ## Configuration example 23 | Add to application config folowing rules: 24 | ```php 25 | [ 26 | 'components' => [ 27 | 'oData' => [ 28 | 'class' => \execut\oData\Client::class, 29 | 'host' => $odataHost, 30 | 'path' => $odataPath, 31 | 'options' => [ 32 | 'auth' => [ 33 | $odataLogin, 34 | $odataPassword, 35 | ], 36 | ], 37 | 'customColumnsTypes' => [ 38 | // Here you custom columns types stubs configuration. Example: 39 | 'Catalog_Контрагенты' => [ 40 | 'НаименованиеПолное' => 'text', 41 | ], 42 | ], 43 | ], 44 | ], 45 | ]; 46 | ``` 47 | 48 | After configuration, you must declare your models and queries on the basis of two classes: 49 | execut\oData\ActiveRecord and execut\oData\ActiveQuery 50 | 51 | Example model for standard document ЧекККМ ([source here](https://github.com/execut/yii2-1c-odata/tree/master/docs/models)): 52 | ```php 53 | use execut\oData\ActiveRecord; 54 | 55 | class CheckKkm extends ActiveRecord 56 | { 57 | public $complexRelations = [ 58 | 'Оплата', 59 | 'Заказы' 60 | ]; 61 | 62 | public function getОплата() { 63 | return $this->hasMany(CheckKkmPayment::class, [ 64 | 'Ref_Key' => 'Ref_Key', 65 | ]); 66 | } 67 | 68 | public static function tableName() 69 | { 70 | return 'Document_ЧекККМ'; 71 | } 72 | } 73 | 74 | $check = CheckKkm::find()->andWhere([ 75 | 'Ref_Key' => '00000000-0000-0000-0000-000000000001' 76 | ])->one(); 77 | if ($check) { 78 | $check->attributes = [ 79 | //... 80 | ]; 81 | $check->save(); 82 | } 83 | ``` 84 | 85 | ## Your help was, would be useful 86 | For more information, there is not enough time =( 87 | 88 | ## Planned 89 | * Unit tests cover 90 | * Extending functional to standard oData, without 1C 91 | 92 | ## License 93 | 94 | **yii2-1c-odata** is released under the Apache License Version 2.0. See the bundled `LICENSE.md` for details. 95 | -------------------------------------------------------------------------------- /Schema.php: -------------------------------------------------------------------------------- 1 | client->customColumnsTypes; 28 | } 29 | 30 | protected function getMetadata() { 31 | $cacheKey = __CLASS__; 32 | $cache = \yii::$app->cache; 33 | if ($result = $cache->get($cacheKey)) { 34 | return $result; 35 | } 36 | 37 | if ($this->client->isConnectionError()) { 38 | return false; 39 | } 40 | $metadata = $this->client->getMetadata(); 41 | $result = array_merge($metadata['EntityType'], $metadata['ComplexType']); 42 | $cache->set($cacheKey, $result); 43 | 44 | return $result; 45 | } 46 | 47 | public function getTableSchema($name, $refresh = false) 48 | { 49 | $cache = \yii::$app->cache; 50 | $cacheKey = __CLASS__ . __FUNCTION__ . var_export($this->getCustomColumnsTypes(), true) . '14' . $name; 51 | if ($schema = $cache->get($cacheKey)) { 52 | return $schema; 53 | } 54 | 55 | $metadata = $this->getMetadata(); 56 | $name = preg_replace('/\(\)$/', '', str_replace('/', '_', $name)); 57 | $name = str_replace('_BalanceAndTurnovers', '_BalanceAndTurnover', $name); 58 | 59 | foreach ($metadata as $params) { 60 | if ($params['@attributes']['Name'] === $name) { 61 | break; 62 | } 63 | } 64 | 65 | if (empty($params['Key'])) { 66 | $primaryKey = ['Ref_Key', 'LineNumber']; 67 | } else if (empty($params['Key']['PropertyRef']['@attributes'])) { 68 | $primaryKey = []; 69 | foreach ($params['Key']['PropertyRef'] as $properyRef) { 70 | $primaryKey[] = $properyRef['@attributes']['Name']; 71 | } 72 | } else { 73 | $primaryKey = $params['Key']['PropertyRef']['@attributes']; 74 | $primaryKey = array_values($primaryKey); 75 | } 76 | 77 | $columns = []; 78 | foreach ($params['Property'] as $property) { 79 | $property = $property['@attributes']; 80 | if (strpos($property['Type'], 'Collection') !== false) { 81 | continue; 82 | } 83 | 84 | $customColumnsTypes = $this->getCustomColumnsTypes(); 85 | if (!empty($customColumnsTypes[$name]) && !empty($customColumnsTypes[$name][$property['Name']])) { 86 | $type = $customColumnsTypes[$name][$property['Name']]; 87 | } else { 88 | $type = $this->getColumnDbType($property['Type']); 89 | } 90 | 91 | $column = new ColumnSchema([ 92 | 'name' => $property['Name'], 93 | 'type' => $this->getColumnAbstractType($property['Type']), 94 | 'allowNull' => $property['Nullable'] === 'true', 95 | 'phpType' => $this->getColumnPhpType($property['Type']), 96 | 'dbType' => $type, 97 | ]); 98 | $columns[$column->name] = $column; 99 | } 100 | 101 | $tableSchema = new TableSchema([ 102 | 'name' => $name, 103 | 'fullName' => $name, 104 | 'primaryKey' => $primaryKey, 105 | 'columns' => $columns 106 | ]); 107 | 108 | $cache->set($cacheKey, $tableSchema); 109 | 110 | return $tableSchema; 111 | } 112 | 113 | protected function getColumnDbType($type) { 114 | return $type; 115 | } 116 | 117 | protected function getColumnAbstractType($type) { 118 | return $this->getColumnPhpType($type); 119 | } 120 | 121 | protected function getColumnPhpType($type) { 122 | $typesMap = [ 123 | 'Edm.Guid' => 'string', 124 | 'Edm.Boolean' => 'boolean', 125 | 'Edm.String' => 'string', 126 | 'Edm.Int16' => 'int', 127 | 'Edm.Int32' => 'int', 128 | 'Edm.Int64' => 'int', 129 | 'Edm.Double' => 'double', 130 | ]; 131 | if (empty($typesMap[$type])) { 132 | return $type; 133 | } 134 | 135 | return $typesMap[$type]; 136 | } 137 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "execut/yii2-1c-odata", 3 | "description": "Yii2 component for work with 1C oData via activeRecord", 4 | "keywords": ["yii2", "odata", "activeRecord"], 5 | "type": "yii2-extension", 6 | "license": "Apache-2.0", 7 | "support": { 8 | "issues": "https://github.com/execut/yii2-1c-odata/issues", 9 | "source": "https://github.com/execut/yii2-1c-odata" 10 | }, 11 | "authors": [ 12 | { 13 | "name": "eXeCUT" 14 | } 15 | ], 16 | "require": { 17 | "yiisoft/yii2": "*", 18 | "execut/odata-1c": "*" 19 | 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "execut\\oData\\": "" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /docs/models/CheckKkm.php: -------------------------------------------------------------------------------- 1 | hasMany(CheckKkmPayment::class, [ 21 | 'Ref_Key' => 'Ref_Key', 22 | ]); 23 | } 24 | 25 | public static function tableName() 26 | { 27 | return 'Document_ЧекККМ'; 28 | } 29 | 30 | public function getName() { 31 | return $this->__toString(); 32 | } 33 | 34 | public function __toString() 35 | { 36 | return '#' . $this->Ref_Key; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/models/CheckKkmPayment.php: -------------------------------------------------------------------------------- 1 | hasOne(CheckKkm::class, [ 16 | 'Ref_Key' => 'Ref_Key', 17 | ]); 18 | } 19 | 20 | public static function tableName() 21 | { 22 | return 'Document_ЧекККМ_Оплата'; 23 | } 24 | 25 | public function __toString() 26 | { 27 | return '#' . $this->Ref_Key; 28 | } 29 | 30 | public function getName() { 31 | return $this->__toString(); 32 | } 33 | 34 | public function getPrimaryKey($asArray = false) 35 | { 36 | return 'Ref_Key'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /logger/Container.php: -------------------------------------------------------------------------------- 1 | initLoggers(); 19 | foreach ($this->getLoggers() as $logger) { 20 | $logger->begin(); 21 | } 22 | } 23 | 24 | public function end() 25 | { 26 | foreach ($this->getLoggers() as $logger) { 27 | $logger->end(); 28 | } 29 | } 30 | 31 | protected function initLoggers() { 32 | $loggers = $this->loggers; 33 | foreach ($loggers as $key => $logger) { 34 | if (is_array($logger)) { 35 | $loggers[$key] = \yii::createObject($logger); 36 | } 37 | } 38 | 39 | $this->loggers = $loggers; 40 | } 41 | 42 | protected function getLoggers() { 43 | foreach ($this->loggers as $logger) { 44 | $logger->request = $this->request; 45 | } 46 | 47 | return $this->loggers; 48 | } 49 | } -------------------------------------------------------------------------------- /logger/Profiler.php: -------------------------------------------------------------------------------- 1 | getMessage(), 'yii\db\Command::query'); 19 | } 20 | 21 | public function end() 22 | { 23 | \yii::endProfile($this->getMessage(), 'yii\db\Command::query'); 24 | } 25 | 26 | protected function getMessage() { 27 | return trim($this->request->getHost(), '/') . $this->request->getUrl() . ' (' . $this->request->getMethod() . '), options: ' . var_export($this->request->getOptions(), true); 28 | } 29 | } -------------------------------------------------------------------------------- /logger/RequestsMonitor.php: -------------------------------------------------------------------------------- 1 | monitor === null) { 30 | $this->monitor = \yii::createObject([ 31 | 'class' => \execut\requestsMonitor\Monitor::class, 32 | 'maxRequests' => 15, 33 | 'requestTimeLimit' => 30 * 1000 * 1000, 34 | 'requestsStorage' => [ 35 | 'class' => \execut\requestsMonitor\requestsStorage\MutexStorage::class, 36 | 'isCheckProcessesExists' => true, 37 | 'adapter' => [ 38 | 'class' => ActiveRecord::class, 39 | 'defaultAttributes' => [ 40 | 'service_id' => Common::SERVICE_ONE_C, 41 | ] 42 | ] 43 | // 'cacheKey' => 'odataRequestsStorage', 44 | ], 45 | 'handler' => [ 46 | 'class' => \execut\requestsMonitor\handler\ExceptionHandler::class, 47 | 'exceptionClass' => LongRequestsException::class, 48 | 'exceptionMessage' => 'Many long requests to odata. Requests count limit: {maxRequests}, time limit: {requestTimeLimit} useconds, current time {currentTime} useconds', 49 | ] 50 | ]); 51 | } 52 | 53 | return $this->monitor; 54 | } 55 | 56 | protected $requestId = null; 57 | public function begin() 58 | { 59 | $method = Request::getMethodFromString($this->request->getMethod()); 60 | if (!$method) { 61 | throw new Exception('Undefined method ' . $this->request->getMethod()); 62 | } 63 | 64 | $requestParams = [ 65 | 'url' => substr(trim($this->request->getHost(), '/') . $this->request->getUrl(), 0, 2048), 66 | 'post_params' => \mb_substr(serialize($this->request->getOptions()), 0, 8192), 67 | 'method' => $method, 68 | ]; 69 | 70 | $this->requestId = $this->getMonitor()->startRequest($requestParams); 71 | } 72 | 73 | protected function getRequestId() { 74 | return $this->request->getUrl() . '/' . $this->request->getMethod() . '/' . serialize($this->request->getOptions()) . '/' . microtime(); 75 | } 76 | 77 | public function end() 78 | { 79 | $this->getMonitor()->finishRequest($this->requestId); 80 | } 81 | } --------------------------------------------------------------------------------