the browser-facing portion of osu!
at master 8.4 kB view raw
1<?php 2 3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0. 4// See the LICENCE file in the repository root for full licence text. 5 6namespace App\Models; 7 8use App\Exceptions\ModelNotSavedException; 9use App\Libraries\MorphMap; 10use App\Libraries\Transactions\AfterCommit; 11use App\Libraries\Transactions\AfterRollback; 12use App\Libraries\TransactionStateManager; 13use App\Scopes\MacroableModelScope; 14use App\Traits\Validatable; 15use Exception; 16use Illuminate\Database\ClassMorphViolationException; 17use Illuminate\Database\Eloquent\Factories\HasFactory; 18use Illuminate\Database\Eloquent\Model as BaseModel; 19 20abstract class Model extends BaseModel 21{ 22 use HasFactory, Traits\FasterAttributes, Validatable; 23 24 const MAX_FIELD_LENGTHS = []; 25 const int PER_PAGE = 50; 26 27 protected $connection = 'mysql'; 28 protected $guarded = []; 29 protected array $macros = []; 30 protected $perPage = self::PER_PAGE; 31 protected $primaryKeys; 32 33 public static function booted() 34 { 35 static::addGlobalScope(new MacroableModelScope()); 36 } 37 38 protected static function searchQueryAndParams(array $params) 39 { 40 $limit = \Number::clamp(get_int($params['limit'] ?? null) ?? static::PER_PAGE, 5, 50); 41 $page = max(get_int($params['page'] ?? null), 1); 42 43 $offset = max_offset($page, $limit); 44 $page = 1 + $offset / $limit; 45 46 $query = static::limit($limit)->offset($offset); 47 48 return [$query, compact('limit', 'page')]; 49 } 50 51 public function getForeignKey() 52 { 53 if ($this->primaryKey === null || $this->primaryKey === 'id') { 54 return parent::getForeignKey(); 55 } 56 57 return $this->primaryKey; 58 } 59 60 public function getKey() 61 { 62 return $this->getRawAttribute($this->primaryKey); 63 } 64 65 public function getMacros(): array 66 { 67 return [ 68 ...$this->macros, 69 'getWithHasMore', 70 'last', 71 'realCount', 72 ]; 73 } 74 75 public function getMorphClass() 76 { 77 $className = static::class; 78 79 $ret = MorphMap::getType($className); 80 81 if ($ret !== null) { 82 return $ret; 83 } 84 85 throw new ClassMorphViolationException($this); 86 } 87 88 /** 89 * Locks the current model for update with `select for update`. 90 * 91 * @return $this 92 */ 93 public function lockSelf() 94 { 95 return $this->lockForUpdate()->find($this->getKey()); 96 } 97 98 public function macroGetWithHasMore() 99 { 100 return function ($query) { 101 $limit = $query->getQuery()->limit; 102 if ($limit === null) { 103 throw new Exception('"getWithHasMore" was called on query without "limit" specified'); 104 } 105 $moreLimit = $limit + 1; 106 $result = $query->limit($moreLimit)->get(); 107 108 $hasMore = $result->count() === $moreLimit; 109 110 if ($hasMore) { 111 $result->pop(); 112 } 113 114 return [$result, $hasMore]; 115 }; 116 } 117 118 public function macroLast() 119 { 120 return function ($baseQuery, $column = null) { 121 $query = clone $baseQuery; 122 123 return $query->orderBy($column ?? $this->getKeyName(), 'DESC')->first(); 124 }; 125 } 126 127 public function macroRealCount() 128 { 129 return function ($baseQuery) { 130 $query = clone $baseQuery; 131 $query->unorder(); 132 $query->getQuery()->offset = null; 133 $query->limit(null); 134 135 return min($query->count(), $GLOBALS['cfg']['osu']['pagination']['max_count']); 136 }; 137 } 138 139 public function refresh() 140 { 141 if (method_exists($this, 'resetMemoized')) { 142 $this->resetMemoized(); 143 } 144 145 return parent::refresh(); 146 } 147 148 public function scopeReorderBy($query, $field, $order) 149 { 150 return $query->unorder()->orderBy($field, $order); 151 } 152 153 public function scopeOrderByField($query, $field, $ids) 154 { 155 $size = count($ids); 156 157 if ($size === 0) { 158 return; 159 } 160 161 $bind = implode(',', array_fill(0, $size, '?')); 162 $string = "FIELD({$field}, {$bind})"; 163 $values = array_map('strval', $ids); 164 165 $query->orderByRaw($string, $values); 166 } 167 168 public function scopeNone($query) 169 { 170 $query->whereRaw('false'); 171 } 172 173 public function scopeUnorder($query) 174 { 175 $query->getQuery()->orders = null; 176 177 return $query; 178 } 179 180 public function scopeWithPresent($query, $column) 181 { 182 $query->whereNotNull($column)->where($column, '<>', ''); 183 } 184 185 /** 186 * Just like decrement but only works on saved instance instead of falling back to entire model 187 */ 188 public function decrementInstance() 189 { 190 if (!$this->exists) { 191 return false; 192 } 193 194 return $this->decrement(...func_get_args()); 195 } 196 197 public function delete() 198 { 199 return $this->runAfterCommitWrapper(function () { 200 return parent::delete(); 201 }); 202 } 203 204 /** 205 * Just like increment but only works on saved instance instead of falling back to entire model 206 */ 207 public function incrementInstance() 208 { 209 if (!$this->exists) { 210 return false; 211 } 212 213 return $this->increment(...func_get_args()); 214 } 215 216 public function save(array $options = []) 217 { 218 return $this->runAfterCommitWrapper(function () use ($options) { 219 return parent::save($options); 220 }); 221 } 222 223 public function saveOrExplode($options = []) 224 { 225 return $this->getConnection()->transaction(function () use ($options) { 226 $result = $this->save($options); 227 228 if ($result === false) { 229 $errors = $this->validationErrors(); 230 $message = $errors->isEmpty() ? 'failed saving model' : $errors->toSentence(); 231 232 throw new ModelNotSavedException($message); 233 } 234 235 return $result; 236 }); 237 } 238 239 public function dbName() 240 { 241 $connection = $this->connection ?? $GLOBALS['cfg']['database']['default']; 242 243 return $GLOBALS['cfg']['database']['connections'][$connection]['database']; 244 } 245 246 public function tableName(bool $includeDbPrefix = false) 247 { 248 return ($includeDbPrefix ? $this->dbName().'.' : '').$this->getTable(); 249 } 250 251 // Allows save/update/delete to work with composite primary keys. 252 // Note this doesn't fix 'find' method and a bunch of other laravel things 253 // which rely on getKeyName and getKey (and they themselves are broken as well). 254 protected function setKeysForSaveQuery($query) 255 { 256 if (isset($this->primaryKeys)) { 257 foreach ($this->primaryKeys as $key) { 258 $query->where([$key => $this->original[$key] ?? null]); 259 } 260 261 return $query; 262 } 263 264 return parent::setKeysForSaveQuery($query); 265 } 266 267 // same deal with setKeysForSaveQuery but for select query 268 protected function setKeysForSelectQuery($query) 269 { 270 return $this->setKeysForSaveQuery($query); 271 } 272 273 protected function validateDbFieldLength(int $limit, string $dbField, ?string $checkField = null): void 274 { 275 if ($this->isDirty($dbField)) { 276 $this->validateFieldLength($limit, $dbField, $checkField); 277 } 278 } 279 280 protected function validateDbFieldLengths(): void 281 { 282 foreach (static::MAX_FIELD_LENGTHS as $field => $limit) { 283 $this->validateDbFieldLength($limit, $field, $field); 284 } 285 } 286 287 protected function validationErrorsTranslationPrefix(): string 288 { 289 return ''; 290 } 291 292 private function enlistCallbacks($model, $connection) 293 { 294 $transaction = resolve(TransactionStateManager::class)->current($connection); 295 if ($model instanceof AfterCommit) { 296 $transaction->addCommittable($model); 297 } 298 299 if ($model instanceof AfterRollback) { 300 $transaction->addRollbackable($model); 301 } 302 303 return $transaction; 304 } 305 306 private function runAfterCommitWrapper(callable $fn) 307 { 308 $transaction = $this->enlistCallbacks($this, $this->connection); 309 310 $result = $fn(); 311 312 if ($this instanceof AfterCommit && $transaction->isReal() === false) { 313 $transaction->commit(); 314 } 315 316 return $result; 317 } 318}