1: <?php
2:
3: namespace ZDB ;
4:
5: /**
6: * Базовый класс для бизнес-сущностей
7: * Реализует паттерн Active Directory
8: * Предназначен для автоматизации стандартных над записями в БД
9: */
10: abstract class Entity
11: {
12: protected $fields = array(); //список полей
13:
14: /**
15: * Конструктор
16: *
17: * @param mixed $row массив инициализирующий некторые
18: * или все поля объекта
19: *
20: */
21:
22: public function __construct($row = null) {
23: $this->init();
24: if (is_array($row)) {
25: $this->fields = array_merge($this->fields, $row);
26: }
27: }
28:
29: /**
30: * Инициализация полей сущности
31: *
32: */
33: protected function init() {
34: $meta = $this->getMetadata();
35: $this->{$meta['keyfield']} = 0;
36: }
37:
38: /**
39: * Возврашает метаданные для выборки из БД
40: * Реализуется конкретными сущностями имплементирующими класс Entity
41: * Метаданные содержат имя таблицы, имя ключевого поля
42: * а также имя представления если такое существует в БД
43: * Например array('table' => 'system_users','view' => 'system_users_view', 'keyfield' => 'user_id')
44: *
45: * Вместо испоользования метода можно импользоввать аннтации возде определения класса
46: * анноации именуются аналогично ключам массива метаданных.
47: */
48: protected static function getMetadata() {
49: $class = new \ReflectionClass(get_called_class());
50: $doc = $class->getDocComment();
51: preg_match_all('/@([a-z0-9_-]+)=([^\n]+)/is', $doc, $arr);
52: if (is_array($arr)) {
53: $reg_arr = array_combine($arr[1], $arr[2]);
54:
55: $table = trim($reg_arr['table']);
56: $view = trim($reg_arr['view'] ?? '');
57: $keyfield = trim($reg_arr['keyfield']);
58:
59:
60: if (strlen($table) > 0 && strlen($keyfield) > 0) {
61: $retarr = array();
62: $retarr['table'] = $table;
63: $retarr['keyfield'] = $keyfield;
64: if (strlen($view) > 0) {
65: $retarr['view'] = $view;
66: }
67:
68: return $retarr;
69: }
70: }
71:
72:
73: throw new ZDBException('getMetadata должен быть перегружен');
74: }
75:
76: /**
77: * Возвращает сущность из БД по ключу
78: * @param mixed $param
79: * @param mixed $fields уточнение списка возвращаемых полей По умолчанию ставится *
80: */
81: public static function load($param, $fields='*') {
82: if($param ==null) {
83: return null;
84: }
85: if(is_string($param )) {
86: if(strlen($param )==0) {
87: return null;
88: }
89: }
90: $row = array();
91: $class = get_called_class();
92:
93: $meta = $class::getMetadata();
94: $table = isset($meta['view']) ? $meta['view'] : $meta['table'];
95: $conn = DB::getConnect();
96: if (is_numeric($param)) {
97: $row = $conn->GetRow("select {$fields} from {$table} where {$meta['keyfield']} = " . $param);
98: } elseif (is_array($param)) {
99: $row = $param;
100: }
101:
102: if (count($row) == 0) {
103: return null;
104: }
105: $obj = new $class();
106:
107: $obj->setData($row);
108: $obj->afterLoad();
109: return $obj;
110: }
111:
112: /**
113: * Возвращает количество сущностй в БД по критерию
114: *
115: * @param mixed $where
116: */
117: public static function findCnt($where = "") {
118: $class = get_called_class();
119: $meta = $class::getMetadata();
120: $conn = DB::getConnect();
121:
122: $table = isset($meta['view']) ? $meta['view'] : $meta['table'];
123: $sql = "select coalesce(count({$meta['keyfield']}),0) as cnt from " . $table;
124:
125: $cnst = static::getConstraint();
126: if(strlen($cnst) >0) {
127: if(strlen($where)==0) {
128: $where = $cnst;
129: } else {
130: $where = "({$cnst}) and ({$where}) ";
131: }
132: }
133: if (strlen($where) > 0) {
134: $sql .= " where " . $where;
135: }
136:
137:
138: return $conn->getOne($sql);
139: }
140:
141:
142:
143:
144: public static function findBySql($sql) {
145:
146: $class = get_called_class();
147: $meta = $class::getMetadata();
148: //$table = isset($meta['view']) ? $meta['view'] : $meta['table'];
149: $conn = DB::getConnect();
150: $list = array();
151:
152: $rs = $conn->Execute($sql);
153:
154: foreach ($rs as $row) {
155: $item = new $class();
156: $item->setData($row);
157: $list[$row[$meta['keyfield']]] = $item;
158: $list[$row[$meta['keyfield']]]->afterLoad();
159: }
160: return $list;
161: }
162:
163: /**
164: * Возвращает массив ключ/имя из БД по критерию
165: * Может использоватся для заполнения выпадающих списков
166: *
167: * @param string $fieldname Имя поля представляющее поле сущности. Можно использовать конкатенацию полей.
168: * @param mixed $where Условие для предиката where
169: * @param mixed $orderbyfield
170: * @param mixed $orderbydir
171: * @param mixed $count
172: * @param mixed $offset
173: */
174: public static function findArray($fieldname, $where = '', $orderbyfield = null, $count = -1, $offset = -1) {
175:
176: $class = get_called_class();
177: $meta = $class::getMetadata();
178: $table = isset($meta['view']) ? $meta['view'] : $meta['table'];
179: $conn = DB::getConnect();
180:
181: $sql = "select {$meta['keyfield']} ,{$fieldname} as _field_ from " . $table;
182:
183:
184: $cnst = static::getConstraint();
185: if(strlen($cnst ?? '') >0) {
186: if(strlen($where ?? '')==0) {
187: $where = $cnst;
188: } else {
189: $where = "({$cnst}) and ({$where}) ";
190:
191: }
192: }
193:
194: if (strlen(trim($where ?? '')) > 0) {
195: $sql .= " where " . $where;
196: }
197: $orderbyfield = trim($orderbyfield ?? '') ;
198: if(trim($orderbyfield)=='asc') {
199: $orderbyfield='';
200: }
201: if(trim($orderbyfield)=='desc') {
202: $orderbyfield='';
203: }
204: if (strlen($orderbyfield) > 0) {
205: $sql .= " order by " . $orderbyfield;
206: }
207:
208: if ($offset >= 0 or $count >= 0) {
209: $rs = $conn->SelectLimit($sql, $count, $offset);
210: } else {
211: $rs = $conn->Execute($sql);
212: }
213: $list = array();
214: foreach ($rs as $row) {
215:
216: $list[$row[$meta['keyfield']]] = $row['_field_'];
217:
218: }
219: return $list;
220: }
221:
222:
223:
224: /**
225: * Возвращает массив сущностей из БД по критерию
226: *
227: * @param mixed $where Условие для предиката where
228: * @param mixed $orderbyfield
229: * @param mixed $orderbydir
230: * @param mixed $count
231: * @param mixed $offset
232: * @param mixed $fields уточнение списка возвращаемых полей По умолчанию ставится *
233: * @return массив
234: */
235: public static function find($where = '', $orderbyfield = null, $count = -1, $offset = -1, $fields='') : array {
236: $list = [];
237:
238: foreach( self::findYield($where , $orderbyfield , $count , $offset , $fields) as $k=>$v ){
239: $list[$k]=$v ;
240: }
241:
242: return $list;
243: }
244:
245: /**
246: * Возвращает итерируемый набор сущностей из БД по критерию
247: *
248: * @param mixed $where Условие для предиката where
249: * @param mixed $orderbyfield
250: * @param mixed $orderbydir
251: * @param mixed $count
252: * @param mixed $offset
253: * @param mixed $fields уточнение списка возвращаемых полей По умолчанию ставится *
254: * @return итератор
255: */
256: public static function findYield($where = '', $orderbyfield = null, $count = -1, $offset = -1, $fields='') {
257: if(strlen($fields)==0) {
258: $fields ="*";
259: }
260:
261: $class = get_called_class();
262: $meta = $class::getMetadata();
263: $table = isset($meta['view']) ? $meta['view'] : $meta['table'];
264: $conn = DB::getConnect();
265: $list = array();
266: $sql = "select {$fields} from " . $table;
267:
268: $cnst = static::getConstraint();
269: if(strlen($cnst ?? '') >0) {
270: if(strlen($where ?? '')==0) {
271: $where = $cnst;
272: } else {
273: $where = "({$cnst}) and ({$where}) ";
274:
275: }
276: }
277:
278: if (strlen(trim($where ?? '')) > 0) {
279: $sql .= " where " . $where;
280: }
281: $orderbyfield = trim($orderbyfield ?? '') ;
282: if(trim($orderbyfield ?? '')=='asc') {
283: $orderbyfield='';
284: }
285: if(trim($orderbyfield ?? '')=='desc') {
286: $orderbyfield='';
287: }
288: if (strlen($orderbyfield ?? '') > 0) {
289: $sql .= " order by " . $orderbyfield;
290: }
291:
292: if ($offset >= 0 or $count >= 0) {
293: $rs = $conn->SelectLimit($sql, $count, $offset);
294: } else {
295: $rs = $conn->Execute($sql);
296: }
297:
298: foreach ($rs as $row) {
299: $item = new $class();
300: $item->setData($row);
301: $item->afterLoad();
302: yield $row[$meta['keyfield']] => $item;
303:
304: }
305:
306: }
307:
308:
309:
310: /**
311: * Возвращает одно скалярное значение из одной строки
312: * @param mixed $field возвращаемое поле или выражение
313: * @param mixed $where
314: */
315: public static function getOne($field, $where = "") {
316: $class = get_called_class();
317: $meta = $class::getMetadata();
318: $conn = DB::getConnect();
319:
320: $table = isset($meta['view']) ? $meta['view'] : $meta['table'];
321: $sql = "select {$field} from " . $table;
322:
323: $cnst = static::getConstraint();
324: if(strlen($cnst) >0) {
325: if(strlen($where)==0) {
326: $where = $cnst;
327: } else {
328: $where = "({$cnst}) and ({$where}) ";
329:
330: }
331: }
332:
333: if (strlen($where) > 0) {
334: $sql .= " where " . $where;
335: }
336:
337:
338: return $conn->getOne($sql);
339: }
340: /**
341: * Возвращает первую строку из набора
342: * @param mixed $where
343: * @param mixed $orderbyfield
344: * @param mixed $unique если true должна быть только одна запись
345: */
346: public static function getFirst($where = "", $orderbyfield = null, $fields='') {
347: $list = self::find($where, $orderbyfield, 1, -1, $fields);
348:
349: if (count($list) == 0) {
350: return null;
351: }
352:
353: return array_pop($list);
354:
355:
356: }
357:
358: /**
359: * Удаление сущности
360: * возвращает строку с ошибкой если удаление неудачно или не разрешено
361: * @param mixed $id Уникальный ключ
362: */
363: public static function delete($id) {
364: $class = get_called_class();
365: $meta = $class::getMetadata();
366:
367: if ($id>0) {
368:
369: $obj = $class::load($id);
370: }
371:
372: if ($obj instanceof Entity) {
373: $allowdelete = $obj->beforeDelete();
374: if (strlen($allowdelete)>0) {
375:
376: return $allowdelete;
377: }
378:
379: $sql = "delete from {$meta['table']} where {$meta['keyfield']} = " . $id;
380: $conn = DB::getConnect();
381: $conn->Execute($sql);
382: $obj->afterDelete();
383: return "";
384: }
385:
386:
387: }
388:
389: /**
390: * Вызывается перед удалением сущности
391: * если возвращает строку с ошибкой удаление отменяется
392: *
393: */
394: protected function beforeDelete() {
395: return "";
396: }
397:
398:
399: /**
400: * Вызывается после удаления
401: *
402: */
403: protected function afterDelete() {
404:
405: }
406:
407:
408:
409:
410: /**
411: * Обработка строки перед вставкой в запрос
412: * после обработки строка не требует кавычек
413: * @param mixed $str
414: */
415: public static function qstr($str) {
416: $conn = DB::getConnect();
417: return $conn->qstr($str);
418: }
419:
420: /**
421: * Добавление слешей в строку
422: *
423: * @param mixed $str
424: */
425: public static function escape($str) {
426: $conn = DB::getConnect();
427: return mysqli_real_escape_string($conn->_connectionID, $str);
428: }
429:
430: /**
431: * Форматирование даты в сответствии с SQL диалектом
432: *
433: * @param mixed $dt Timestamp
434: */
435: public static function dbdate($dt) {
436: $conn = DB::getConnect();
437: return $conn->DBDate($dt);
438: }
439:
440: /**
441: * Возвращает значение поля
442: *
443: * @param mixed $name
444: * @return mixed
445: */
446: final public function __get($name) {
447: return $this->fields[$name] ?? null;
448: }
449:
450: /**
451: * Устанавливает значение поля
452: *
453: * @param mixed $name
454: * @param mixed $value
455: */
456: final public function __set($name, $value) {
457: $this->fields[$name] = $value;
458: }
459:
460: /**
461: * Возвращает поля сущности в виде ассоциативного массива
462: *
463: */
464: final public function getData() {
465: return $this->fields;
466: }
467: /**
468: * записывает данные в сущность
469: *
470: */
471: final public function setData($row) {
472: if (is_array($row)) {
473: $this->fields = array_merge($this->fields, $row);
474: }
475:
476: }
477:
478: /**
479: * Возвращает значение уникального ключа сущности
480: *
481: */
482: final public function getKeyValue() {
483: $meta = $this->getMetadata();
484: return $this->fields[$meta['keyfield']];
485: }
486:
487: /**
488: * Сохраняет сущность в БД
489: * Если сущность новая создает запись
490: *
491: */
492: public function save() {
493:
494: if ($this->beforeSave() === false) {
495: return;
496: };
497: $conn = DB::getConnect();
498: $meta = $this->getMetadata();
499: $flist=$this->fields ;
500: unset($flist[$meta['keyfield']]);//убираем ключевое поле с запроса
501: if (($this->fields[$meta['keyfield']]?? 0) > 0) {
502:
503: $conn->AutoExecute($meta['table'], $flist, "UPDATE", "{$meta['keyfield']} = " . $this->fields[$meta['keyfield']]);
504: $this->afterSave(true);
505: } else {
506: $conn->AutoExecute($meta['table'], $flist, "INSERT");
507: $this->fields[$meta['keyfield']] = $conn->Insert_ID();
508: $this->afterSave(false);
509: }
510: }
511:
512: /**
513: * Вызывается перед сохранением сущности
514: * Если возвращает false сохранение отменяется
515: *
516: */
517: protected function beforeSave() {
518: return true;
519: }
520:
521: /**
522: * Вызывается после сохранения сущности
523: *
524: * @param mixed $update - true если обновление
525: */
526: protected function afterSave($update) {
527:
528: }
529:
530: /**
531: * Вызывается после загрузки сущности из БД
532: *
533: */
534: protected function afterLoad() {
535:
536: }
537:
538: /**
539: * возвращает ограничение на выборку на уровне бизнес-сущности.
540: * например если в системе нужно ограничить возвращаемый набор для всех выборок
541: * перегружается в класе сущности и возвращает инструкцию для where
542: */
543: protected static function getConstraint() {
544: return '';
545: }
546:
547:
548: }
549:
550: class ZDBException extends \Error
551: {
552: }
553: