Model.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. <?php
  2. /**
  3. * This file is part of the ForkBB <https://github.com/forkbb>.
  4. *
  5. * @copyright (c) Visman <mio.visman@yandex.ru, https://github.com/MioVisman>
  6. * @license The MIT License (MIT)
  7. */
  8. declare(strict_types=1);
  9. namespace ForkBB\Models\PM;
  10. use ForkBB\Core\Container;
  11. use ForkBB\Models\DataModel;
  12. use ForkBB\Models\Model as ParentModel;
  13. use ForkBB\Models\PM\Cnst;
  14. use ForkBB\Models\PM\PBlock;
  15. use ForkBB\Models\PM\PPost;
  16. use ForkBB\Models\PM\PTopic;
  17. use ForkBB\Models\User\Model as User;
  18. use InvalidArgumentException;
  19. use RuntimeException;
  20. class Model extends ParentModel
  21. {
  22. /**
  23. * @var array
  24. */
  25. protected $repository;
  26. public function __construct(Container $container)
  27. {
  28. parent::__construct($container);
  29. $this->zDepend = [
  30. 'area' => ['numPages', 'pagination'],
  31. 'page' => ['pagination'],
  32. ];
  33. $this->repository = [
  34. Cnst::PTOPIC => [],
  35. Cnst::PPOST => [],
  36. ];
  37. }
  38. protected function checkType(int $type, DataModel $model = null): void
  39. {
  40. switch ($type) {
  41. case Cnst::PTOPIC:
  42. if (
  43. null === $model
  44. || $model instanceof PTopic
  45. ) {
  46. return;
  47. }
  48. break;
  49. case Cnst::PPOST:
  50. if (
  51. null === $model
  52. || $model instanceof PPost
  53. ) {
  54. return;
  55. }
  56. break;
  57. }
  58. throw new InvalidArgumentException("Wrong type: {$type}");
  59. }
  60. public function get(int $type, int $key): ?DataModel
  61. {
  62. $this->checkType($type);
  63. return $this->repository[$type][$key] ?? null;
  64. }
  65. public function set(int $type, int $key, /* mixed */ $value): self
  66. {
  67. $this->checkType($type);
  68. $this->repository[$type][$key] = $value;
  69. return $this;
  70. }
  71. public function isset(int $type, int $key): bool
  72. {
  73. $this->checkType($type);
  74. return \array_key_exists($key, $this->repository[$type]);
  75. }
  76. public function create(int $type, array $attrs = []): DataModel
  77. {
  78. switch ($type) {
  79. case Cnst::PTOPIC:
  80. return $this->c->PTopicModel->setAttrs($attrs);
  81. case Cnst::PPOST:
  82. return $this->c->PPostModel->setAttrs($attrs);
  83. default:
  84. throw new InvalidArgumentException("Wrong type: {$type}");
  85. }
  86. }
  87. public function accessTopic(int $id): bool
  88. {
  89. return isset($this->idsCurrent[$id]) || isset($this->idsArchive[$id]);
  90. }
  91. public function inArea(PTopic $topic): ?string
  92. {
  93. if (isset($this->idsArchive[$topic->id])) {
  94. return Cnst::ACTION_ARCHIVE;
  95. } elseif (isset($this->idsNew[$topic->id])) {
  96. return Cnst::ACTION_NEW;
  97. } elseif (isset($this->idsCurrent[$topic->id])) {
  98. return Cnst::ACTION_CURRENT;
  99. } else {
  100. return null;
  101. }
  102. }
  103. public function load(int $type, int $id): ?DataModel
  104. {
  105. $this->checkType($type);
  106. if ($this->isset($type, $id)) {
  107. return $this->get($type, $id);
  108. }
  109. switch ($type) {
  110. case Cnst::PTOPIC:
  111. if ($this->accessTopic($id)) {
  112. $model = $this->Load->load($type, $id);
  113. } else {
  114. $model = null;
  115. }
  116. break;
  117. case Cnst::PPOST:
  118. $model = $this->Load->load($type, $id);
  119. if (
  120. $model instanceof PPost
  121. && ! $this->accessTopic($model->topic_id)
  122. ) {
  123. $model = null;
  124. }
  125. break;
  126. }
  127. $this->set($type, $id, $model);
  128. return $model;
  129. }
  130. public function loadByIds(int $type, array $ids): array
  131. {
  132. $this->checkType($type);
  133. $result = [];
  134. $data = [];
  135. foreach ($ids as $id) {
  136. if ($this->isset($type, $id)) {
  137. $result[$id] = $this->get($type, $id);
  138. } else {
  139. switch ($type) {
  140. case Cnst::PTOPIC:
  141. if (! $this->accessTopic($id)) {
  142. break;
  143. }
  144. default:
  145. $data[] = $id;
  146. }
  147. $result[$id] = null;
  148. $this->set($type, $id, null);
  149. }
  150. }
  151. if (empty($data)) {
  152. return $result;
  153. }
  154. foreach ($this->Load->loadByIds($type, $data) as $model) {
  155. if ($model instanceof PPost) {
  156. if (! $this->accessTopic($model->topic_id)) {
  157. continue;
  158. }
  159. } elseif (! $model instanceof PTopic) {
  160. continue;
  161. }
  162. $result[$model->id] = $model;
  163. $this->set($type, $model->id, $model);
  164. }
  165. return $result;
  166. }
  167. public function update(int $type, DataModel $model): DataModel
  168. {
  169. $this->checkType($type, $model);
  170. return $this->Save->update($model);
  171. }
  172. public function insert(int $type, DataModel $model): int
  173. {
  174. $this->checkType($type, $model);
  175. $id = $this->Save->insert($model);
  176. $this->set($type, $id, $model);
  177. return $id;
  178. }
  179. /**
  180. * Инициализирует массивы индексов приватных тем текущего пользователя
  181. * Инициализирует число приватных тем без фильтра
  182. * Может использовать фильтр по второму пользователю: id или username
  183. */
  184. public function init(/* null|int|string */ $second = null): self
  185. {
  186. list(
  187. $this->idsNew,
  188. $this->idsCurrent,
  189. $this->idsArchive,
  190. $this->totalNew,
  191. $this->totalCurrent,
  192. $this->totalArchive
  193. ) = $this->infoForUser($this->c->user, $second);
  194. $this->second = $second;
  195. $this->numNew = \count($this->idsNew);
  196. $this->numCurrent = \count($this->idsCurrent);
  197. $this->numArchive = \count($this->idsArchive);
  198. return $this;
  199. }
  200. /**
  201. * Возвращает данные по приватным темам (индексы) любого пользователя
  202. */
  203. public function infoForUser(User $user, /* null|int|string */ $second = null): array
  204. {
  205. // deleted // pt_status = PT_DELETED
  206. // unsent // pt_status = PT_NOTSENT
  207. $idsNew = []; // pt_status = PT_NORMAL and last_post > ..._visit
  208. $idsCur = []; // pt_status = PT_NORMAL or last_post > ..._visit
  209. $idsArc = []; // pt_status = PT_ARCHIVE
  210. $totalNew = 0;
  211. $totalCur = 0;
  212. $totalArc = 0;
  213. if (
  214. $user->isGuest
  215. || $user->isUnverified
  216. ) {
  217. return [$idsNew, $idsCur, $idsArc, $totalNew, $totalCur, $totalArc];
  218. }
  219. $vars = [
  220. ':id' => $user->id,
  221. ':norm' => Cnst::PT_NORMAL,
  222. ':arch' => Cnst::PT_ARCHIVE,
  223. ];
  224. $query = 'SELECT pt.poster, pt.poster_id, pt.poster_status, pt.poster_visit,
  225. pt.target, pt.target_id, pt.target_status, pt.target_visit,
  226. pt.id, pt.last_post
  227. FROM ::pm_topics AS pt
  228. WHERE (pt.poster_id=?i:id AND pt.poster_status=?i:norm)
  229. OR (pt.poster_id=?i:id AND pt.poster_status=?i:arch)
  230. OR (pt.target_id=?i:id AND pt.target_status=?i:norm)
  231. OR (pt.target_id=?i:id AND pt.target_status=?i:arch)
  232. ORDER BY pt.last_post DESC';
  233. $stmt = $this->c->DB->query($query, $vars);
  234. while ($row = $stmt->fetch()) {
  235. $id = $row['id'];
  236. $lp = $row['last_post'];
  237. if ($row['poster_id'] === $user->id) {
  238. switch ($row['poster_status']) {
  239. case Cnst::PT_ARCHIVE:
  240. if (
  241. null === $second
  242. || $row['target_id'] === $second
  243. || $row['target'] === $second
  244. ) {
  245. $idsArc[$id] = $lp;
  246. }
  247. ++$totalArc;
  248. break;
  249. case Cnst::PT_NORMAL:
  250. if (
  251. null === $second
  252. || $row['target_id'] === $second
  253. || $row['target'] === $second
  254. ) {
  255. if ($lp > $row['poster_visit']) {
  256. $idsNew[$id] = $lp;
  257. }
  258. $idsCur[$id] = $lp;
  259. }
  260. if ($lp > $row['poster_visit']) {
  261. ++$totalNew;
  262. }
  263. ++$totalCur;
  264. break;
  265. }
  266. } elseif ($row['target_id'] === $user->id) {
  267. switch ($row['target_status']) {
  268. case Cnst::PT_ARCHIVE:
  269. if (
  270. null === $second
  271. || $row['poster_id'] === $second
  272. || $row['poster'] === $second
  273. ) {
  274. $idsArc[$id] = $lp;
  275. }
  276. ++$totalArc;
  277. break;
  278. case Cnst::PT_NORMAL:
  279. if (
  280. null === $second
  281. || $row['poster_id'] === $second
  282. || $row['poster'] === $second
  283. ) {
  284. if ($lp > $row['target_visit']) {
  285. $idsNew[$id] = $lp;
  286. }
  287. $idsCur[$id] = $lp;
  288. }
  289. if ($lp > $row['target_visit']) {
  290. ++$totalNew;
  291. }
  292. ++$totalCur;
  293. break;
  294. }
  295. }
  296. }
  297. return [$idsNew, $idsCur, $idsArc, $totalNew, $totalCur, $totalArc];
  298. }
  299. /**
  300. * Возвращает список приватных тем в зависимости от активной папки
  301. * Номер темы в индексе, а не в значении
  302. */
  303. protected function idsList(): array
  304. {
  305. switch ($this->area) {
  306. case Cnst::ACTION_NEW:
  307. $list = $this->idsNew;
  308. break;
  309. case Cnst::ACTION_ARCHIVE:
  310. $list = $this->idsArchive;
  311. break;
  312. default:
  313. $list = $this->idsCurrent;
  314. }
  315. if (\is_array($list)) {
  316. return $list;
  317. }
  318. throw new RuntimeException('Init() method was not executed');
  319. }
  320. /**
  321. * $this->area = ...
  322. */
  323. protected function setarea(string $area): self
  324. {
  325. switch ($area) {
  326. case Cnst::ACTION_NEW:
  327. case Cnst::ACTION_CURRENT:
  328. case Cnst::ACTION_ARCHIVE:
  329. break;
  330. default:
  331. $area = Cnst::ACTION_CURRENT;
  332. }
  333. $this->setAttr('area', $area);
  334. return $this;
  335. }
  336. /**
  337. * ... = $this->numPages;
  338. */
  339. protected function getnumPages(): int
  340. {
  341. return (int) \ceil((\count($this->idsList()) ?: 1) / $this->c->user->disp_topics);
  342. }
  343. /**
  344. * Статус наличия установленной страницы
  345. */
  346. public function hasPage(): bool
  347. {
  348. return \is_int($this->page) && $this->page > 0 && $this->page <= $this->numPages;
  349. }
  350. /**
  351. * ... = $this->pagination;
  352. */
  353. protected function getpagination(): array
  354. {
  355. return $this->c->Func->paginate(
  356. $this->numPages,
  357. $this->page,
  358. 'PMAction',
  359. [
  360. 'second' => $this->second,
  361. 'action' => $this->area,
  362. 'page' => 'more1', // нестандарная переменная для page
  363. ]
  364. );
  365. }
  366. /**
  367. * Возвращает массив приватных тем с установленной страницы
  368. */
  369. public function pmListCurPage(): array
  370. {
  371. if (! $this->hasPage()) {
  372. throw new InvalidArgumentException('Bad number of displayed page');
  373. }
  374. $ids = \array_slice(
  375. $this->idsList(),
  376. ($this->page - 1) * $this->c->user->disp_topics,
  377. $this->c->user->disp_topics,
  378. true
  379. );
  380. return $this->loadByIds(Cnst::PTOPIC, \array_keys($ids));
  381. }
  382. /**
  383. * Перечитывает данные приватных сообщений пользователя
  384. */
  385. public function recalculate(User $user): void
  386. {
  387. if ($user->isGuest) {
  388. return;
  389. }
  390. list($idsNew, $idsCurrent, $idsArchive, $new, $current, $archive) = $this->infoForUser($user);
  391. $user->u_pm_num_new = $new;
  392. $user->u_pm_num_all = $current;
  393. $this->c->users->update($user);
  394. }
  395. protected function getblock(): PBlock
  396. {
  397. return $this->c->PBlockModel;
  398. }
  399. protected function setblock(): void
  400. {
  401. throw new RuntimeException('Read-only block property');
  402. }
  403. }