Complex classes like SmallFilesQueueTransport often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use SmallFilesQueueTransport, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
7 | class SmallFilesQueueTransport extends AbstractQueueTransport { |
||
8 | const SINGLE_FILE_DB_VERSION = 1; |
||
9 | |||
10 | protected $_message_folder; |
||
11 | protected $_queue_name; |
||
12 | protected $_message_folder_subfolder_count = Queue::DefaultMessageFolderSubfolderCount; |
||
13 | protected $_key_to_file = []; |
||
14 | protected $_is_inotify_enabled; |
||
15 | protected $_chunk_file_postfix = ''; |
||
16 | |||
17 | /** |
||
18 | * @var iMessage[] |
||
19 | */ |
||
20 | protected $_last_consumed_messages = []; |
||
21 | /** |
||
22 | * @var integer[] В keys список файлов, которые были прочитаны |
||
23 | */ |
||
24 | protected $_consumed_filenames = []; |
||
25 | |||
26 | /** |
||
27 | * @param SmallFilesQueueConstructionSettings|object $settings |
||
28 | * |
||
29 | * @throws QueueException |
||
30 | */ |
||
31 | 15 | function __construct($settings) { |
|
32 | 15 | if (!isset($settings->message_folder)) { |
|
33 | 1 | throw new QueueException('message_folder has not been set'); |
|
34 | } |
||
35 | 14 | if (!isset($settings->name)) { |
|
36 | 1 | throw new QueueException('name has not been set'); |
|
37 | } |
||
38 | 13 | $this->_message_folder = $settings->message_folder; |
|
39 | 13 | $this->_queue_name = $settings->name; |
|
40 | 13 | if (isset($settings->message_folder_subfolder_count)) { |
|
41 | $this->_message_folder_subfolder_count = $settings->message_folder_subfolder_count; |
||
42 | } |
||
43 | 13 | if (isset($settings->is_inotify_enabled)) { |
|
44 | $this->_is_inotify_enabled = $settings->is_inotify_enabled; |
||
45 | } else { |
||
46 | 13 | $this->_is_inotify_enabled = function_exists('inotify_init'); |
|
47 | } |
||
48 | 13 | $this->_chunk_file_postfix = '_'.static::generate_rnd_postfix(); |
|
49 | 13 | } |
|
50 | |||
51 | /** |
||
52 | * @param boolean $mode |
||
53 | * |
||
54 | * @hint Ничего не делаем, у этого транспорта нет эксклюзивного режима |
||
55 | * @codeCoverageIgnore |
||
56 | */ |
||
57 | function set_exclusive_mode($mode) { } |
||
58 | |||
59 | /** |
||
60 | * @return string |
||
61 | */ |
||
62 | 139 | function get_queue_name() { |
|
65 | |||
66 | 124 | function save() { |
|
67 | 124 | $this->set_same_time_flag(1); |
|
68 | 124 | if (empty($this->_pushed_for_save)) { |
|
69 | 4 | return; |
|
70 | } |
||
71 | |||
72 | /** |
||
73 | * @var SmallFilesQueueSingleFile $data |
||
74 | */ |
||
75 | 124 | $data = (object) []; |
|
76 | 124 | $data->queue = $this->_pushed_for_save; |
|
|
|||
77 | 124 | $data->queue_name = $this->_queue_name; |
|
78 | 124 | $data->time_create = microtime(true); |
|
79 | 124 | $data->time_last_update = microtime(true); |
|
80 | 124 | $data->version = self::SINGLE_FILE_DB_VERSION; |
|
81 | 124 | $num = mt_rand(0, $this->_message_folder_subfolder_count - 1); |
|
82 | 124 | $folder = $this->_message_folder.'/'.$num; |
|
83 | 124 | FileMutex::create_folders_in_path($folder); |
|
84 | 124 | $filename = sprintf('%s/smartqueue_%s%s.chunk.dat', |
|
85 | 124 | $folder, number_format(microtime(true), 4, '.', ''), $this->_chunk_file_postfix); |
|
86 | |||
87 | 124 | $u = file_put_contents($filename, Queue::serialize($data), LOCK_EX); |
|
88 | 124 | if ($u === false) { |
|
89 | // @codeCoverageIgnoreStart |
||
90 | throw new QueueException('Can not save queue to a file: '.FileMutex::get_last_php_error_as_string()); |
||
91 | // @codeCoverageIgnoreEnd |
||
92 | } |
||
93 | 124 | foreach ($this->_pushed_for_save as $message) { |
|
94 | 124 | $this->_key_to_file[static::get_real_key_for_message($message)] = $filename; |
|
95 | } |
||
96 | 124 | $this->_pushed_for_save = []; |
|
97 | 124 | } |
|
98 | |||
99 | /** |
||
100 | * @param double|integer $wait_time |
||
101 | * |
||
102 | * @return iMessage|object|null |
||
103 | * @throws QueueException |
||
104 | */ |
||
105 | 139 | function consume_next_message($wait_time = -1) { |
|
122 | |||
123 | 2 | function clear_consumed_keys() { |
|
128 | |||
129 | 149 | function __clone() { |
|
130 | 149 | parent::__clone(); |
|
131 | 149 | foreach ($this->_last_consumed_messages as &$message) { |
|
132 | $message = clone $message; |
||
133 | } |
||
134 | 149 | } |
|
135 | |||
136 | /** |
||
137 | * @param double|integer $wait_time |
||
138 | * |
||
139 | * @return iMessage|object|null |
||
140 | */ |
||
141 | 139 | protected function consume_next_message_without_inotify($wait_time = -1) { |
|
142 | 139 | $start = microtime(true); |
|
143 | do { |
||
144 | 139 | for ($i = 0; $i < $this->_message_folder_subfolder_count; $i++) { |
|
145 | 139 | $folder = $this->_message_folder.'/'.$i; |
|
146 | 139 | if (!file_exists($folder) or !is_readable($folder) or !is_dir($folder)) { |
|
147 | 137 | continue; |
|
148 | } |
||
149 | 136 | $event = $this->consume_next_message_without_inotify_folder($folder); |
|
150 | 136 | if (!is_null($event)) { |
|
151 | 136 | return $event; |
|
152 | } |
||
153 | } |
||
154 | 132 | } while (($wait_time == -1) or ($start + $wait_time >= microtime(true))); |
|
155 | |||
156 | 132 | return null; |
|
157 | } |
||
158 | |||
159 | /** |
||
160 | * @param string $folder |
||
161 | * |
||
162 | * @return iMessage|object|null |
||
163 | */ |
||
164 | 136 | protected function consume_next_message_without_inotify_folder($folder) { |
|
165 | 136 | foreach (scandir($folder) as $f) { |
|
166 | 136 | if (in_array($f, ['.', '..']) or !preg_match('|smartqueue_[0-9.]+(_[a-z0-9]+)?\\.chunk\\.dat$|', $f)) { |
|
167 | 136 | continue; |
|
168 | } |
||
169 | 136 | $filename = $folder.'/'.$f; |
|
170 | 136 | if (is_dir($filename) or !is_readable($filename)) { |
|
171 | continue; |
||
172 | } |
||
173 | 136 | $event = $this->consume_next_message_from_file($filename); |
|
174 | 136 | if (!is_null($event)) { |
|
175 | 136 | return $event; |
|
176 | } |
||
177 | } |
||
178 | |||
179 | 131 | return null; |
|
180 | } |
||
181 | |||
182 | |||
183 | /** |
||
184 | * Забираем новые event'ы из файла. Файл должен быть уже существующим, читабельным |
||
185 | * |
||
186 | * Проверка на присутствие в индексе осуществляется в этом файле |
||
187 | * |
||
188 | * @param string $filename |
||
189 | * |
||
190 | * @return iMessage|object|null |
||
191 | */ |
||
192 | 136 | protected function consume_next_message_from_file($filename) { |
|
193 | 136 | if (isset($this->_consumed_filenames[$filename])) { |
|
194 | 131 | return null; |
|
195 | } |
||
196 | |||
197 | 136 | $fi = fopen($filename, 'r'); |
|
198 | 136 | $locked = flock($fi, LOCK_EX | LOCK_NB); |
|
199 | 136 | if (!$locked) { |
|
200 | 1 | fclose($fi); |
|
201 | |||
202 | 1 | return null; |
|
203 | } |
||
204 | |||
205 | 136 | $buf = file_get_contents($filename); |
|
206 | /** |
||
207 | * @var SmallFilesQueueSingleFile $file_data |
||
208 | */ |
||
209 | 136 | $file_data = Queue::unserialize($buf, $is_valid); |
|
210 | 136 | if (!is_object($file_data)) { |
|
211 | // File does not contain Single Queue object |
||
212 | flock($fi, LOCK_UN); |
||
213 | fclose($fi); |
||
214 | |||
215 | return null; |
||
216 | } |
||
217 | 136 | if ($file_data->queue_name != $this->get_queue_name()) { |
|
218 | 2 | flock($fi, LOCK_UN); |
|
219 | 2 | fclose($fi); |
|
220 | |||
221 | 2 | return null; |
|
222 | } |
||
223 | 136 | if ($file_data->version != self::SINGLE_FILE_DB_VERSION) { |
|
224 | flock($fi, LOCK_UN); |
||
225 | fclose($fi); |
||
226 | |||
227 | return null; |
||
228 | } |
||
229 | |||
230 | 136 | $this->_last_consumed_messages = []; |
|
231 | 136 | foreach ($file_data->queue as $message) { |
|
232 | 136 | $key = self::get_real_key_for_message($message); |
|
233 | 136 | if (isset($this->_consumed_keys[$key])) { |
|
234 | 2 | continue; |
|
235 | } |
||
236 | |||
237 | 136 | $message->time_consumed = microtime(true); |
|
238 | 136 | $message->queue = $this; |
|
239 | 136 | $this->_key_to_file[$key] = $filename; |
|
240 | 136 | $this->_last_consumed_messages[] = $message; |
|
241 | 136 | $this->_consumed_keys[$key] = 1; |
|
242 | } |
||
243 | 136 | flock($fi, LOCK_UN); |
|
244 | 136 | fclose($fi); |
|
245 | 136 | $this->_consumed_filenames[$filename] = 1; |
|
246 | |||
247 | 136 | return !empty($this->_last_consumed_messages) ? array_shift($this->_last_consumed_messages) : null; |
|
248 | } |
||
249 | |||
250 | /** |
||
251 | * @param iMessage[]|object[] $messages |
||
252 | */ |
||
253 | 45 | protected function copy_key_to_file_from_messages(array $messages) { |
|
254 | 45 | foreach ($messages as $message) { |
|
255 | 45 | if (isset($message->queue) and (get_class($message->queue) == self::class)) { |
|
256 | 45 | foreach ($message->queue->_key_to_file as $key => $filename) { |
|
257 | 45 | $this->_key_to_file[$key] = $filename; |
|
258 | } |
||
259 | } |
||
260 | } |
||
261 | 45 | } |
|
262 | |||
263 | /** |
||
264 | * @param iMessage[]|object[] $messages |
||
265 | * |
||
266 | * @return array[] |
||
267 | */ |
||
268 | 45 | protected function get_filenames_from_messages(array $messages) { |
|
269 | 45 | $this->copy_key_to_file_from_messages($messages); |
|
270 | 45 | $filenames = []; |
|
271 | 45 | $filenames_contains_keys = []; |
|
272 | 45 | foreach ($messages as $message) { |
|
273 | 45 | $key = self::get_real_key_for_message($message); |
|
274 | 45 | if (!isset($this->_key_to_file[$key])) { |
|
275 | 24 | continue; |
|
276 | } |
||
277 | 45 | $filename = $this->_key_to_file[$key]; |
|
278 | 45 | $filenames[] = $filename; |
|
279 | 45 | if (!isset($filenames_contains_keys[$filename])) { |
|
280 | 45 | $filenames_contains_keys[$filename] = []; |
|
281 | } |
||
282 | |||
283 | 45 | $filenames_contains_keys[$filename][] = $key; |
|
284 | } |
||
285 | |||
286 | 45 | return [$filenames, $filenames_contains_keys]; |
|
287 | } |
||
288 | |||
289 | /** |
||
290 | * Удалить сообщения и сразу же записать это в БД |
||
291 | * |
||
292 | * @param iMessage[]|object[] $messages |
||
293 | * |
||
294 | * @return string[]|integer[] |
||
295 | * @throws QueueException |
||
296 | */ |
||
297 | 45 | function delete_messages(array $messages) { |
|
298 | /** |
||
299 | * @var string[][] $filenames_contains_keys |
||
300 | * @var string[] $filenames |
||
301 | */ |
||
302 | 45 | list($filenames, $filenames_contains_keys) = $this->get_filenames_from_messages($messages); |
|
303 | 45 | $filenames_contains_keys_all = []; |
|
304 | 45 | foreach ($this->_key_to_file as $key => $filename) { |
|
305 | 45 | if (isset($filenames_contains_keys_all[$filename])) { |
|
306 | 33 | $filenames_contains_keys_all[$filename][] = $key; |
|
307 | } else { |
||
308 | 45 | $filenames_contains_keys_all[$filename] = [$key]; |
|
309 | } |
||
310 | } |
||
311 | 45 | unset($key, $filename); |
|
312 | |||
313 | 45 | $deleted_keys = []; |
|
314 | 45 | foreach ($filenames as $filename) { |
|
315 | 45 | if (!file_exists($filename)) { |
|
316 | // @todo Надо подумать правильный ли это подход |
||
317 | // Тут будут все ключи, а не только те, которые надо было удалить |
||
318 | 32 | $deleted_keys = array_merge($deleted_keys, $filenames_contains_keys[$filename]); |
|
319 | 32 | continue; |
|
320 | } |
||
321 | 45 | if (!is_writable($filename)) { |
|
322 | throw new QueueException('Can not delete messages from read only files'); |
||
323 | } |
||
324 | 45 | if (count($filenames_contains_keys[$filename]) == count($filenames_contains_keys_all[$filename])) { |
|
325 | // Нужно удалить все записи в файле, значит можно просто удалить файл целиком |
||
326 | 37 | if (!unlink($filename)) { |
|
327 | // @codeCoverageIgnoreStart |
||
328 | throw new QueueException('Can not delete file '.$filename.': '. |
||
329 | FileMutex::get_last_php_error_as_string()); |
||
330 | // @codeCoverageIgnoreEnd |
||
331 | } |
||
332 | 37 | $deleted_keys = array_merge($deleted_keys, $filenames_contains_keys[$filename]); |
|
333 | 37 | continue; |
|
334 | } |
||
335 | |||
336 | 13 | $fo = fopen($filename, 'r'); |
|
337 | 13 | $locked = flock($fo, LOCK_EX); |
|
338 | 13 | if (!$locked) { |
|
339 | fclose($fo); |
||
340 | throw new QueueException('Can not delete file '.$filename); |
||
341 | } |
||
342 | 13 | $buf = file_get_contents($filename); |
|
343 | 13 | if (empty($buf)) { |
|
344 | flock($fo, LOCK_UN); |
||
345 | fclose($fo); |
||
346 | throw new QueueException('File "'.$filename.'" is empty'); |
||
347 | } |
||
348 | /** |
||
349 | * @var SmallFilesQueueSingleFile $data |
||
350 | */ |
||
351 | 13 | $data = Queue::unserialize($buf, $is_valid); |
|
352 | 13 | if (!is_object($data)) { |
|
353 | flock($fo, LOCK_UN); |
||
354 | fclose($fo); |
||
355 | throw new QueueException('File "'.$filename.'" does not contain Single Queue object'); |
||
356 | } |
||
357 | 13 | if ($data->queue_name != $this->get_queue_name()) { |
|
358 | flock($fo, LOCK_UN); |
||
359 | fclose($fo); |
||
360 | throw new QueueException('Invalid queue name ("'.$data->queue_name. |
||
361 | '" instead of "'.$this->get_queue_name().'")'); |
||
362 | } |
||
363 | 13 | if ($data->version != self::SINGLE_FILE_DB_VERSION) { |
|
364 | flock($fo, LOCK_UN); |
||
365 | fclose($fo); |
||
366 | continue; |
||
367 | } |
||
368 | 13 | $data->time_last_update = microtime(true); |
|
369 | |||
370 | 13 | $new_queue = []; |
|
371 | 13 | foreach ($data->queue as $message) { |
|
372 | 13 | $key = self::get_real_key_for_message($message); |
|
373 | 13 | if (!in_array($key, $filenames_contains_keys[$filename])) { |
|
374 | 13 | $message->is_read = true; |
|
375 | 13 | $new_queue[] = $message; |
|
376 | } else { |
||
377 | 13 | $deleted_keys[] = $key; |
|
378 | } |
||
379 | } |
||
380 | 13 | if (empty($new_queue)) { |
|
381 | // @hint На самом деле это невозможно |
||
382 | 13 | flock($fo, LOCK_UN); |
|
383 | 13 | fclose($fo); |
|
384 | 13 | if (!unlink($filename)) { |
|
385 | // @codeCoverageIgnoreStart |
||
386 | throw new QueueException('Can not delete file '.$filename.': '. |
||
387 | FileMutex::get_last_php_error_as_string()); |
||
388 | // @codeCoverageIgnoreEnd |
||
389 | } |
||
390 | 13 | continue; |
|
391 | } |
||
392 | 13 | $data->queue = $new_queue; |
|
393 | 13 | $u = file_put_contents($filename, Queue::serialize($data)); |
|
394 | 13 | if ($u === false) { |
|
395 | // @codeCoverageIgnoreStart |
||
396 | throw new QueueException('Can not save single query file "'.$filename.'": '. |
||
397 | FileMutex::get_last_php_error_as_string()); |
||
398 | // @codeCoverageIgnoreEnd |
||
399 | } |
||
400 | 13 | flock($fo, LOCK_UN); |
|
401 | 13 | fclose($fo); |
|
402 | } |
||
403 | |||
404 | 45 | return array_unique($deleted_keys); |
|
405 | } |
||
406 | |||
407 | /** |
||
408 | * Обновляем сообщение и сразу же сохраняем всё |
||
409 | * |
||
410 | * Эта функция не рейзит ошибку, если сообщение не найдено |
||
411 | * |
||
412 | * @param iMessage|object $message |
||
413 | * @param string|null $key форсированно задаём ключ сообщения |
||
414 | * |
||
415 | * @return boolean |
||
416 | * @throws QueueException |
||
417 | */ |
||
418 | 80 | function update_message($message, $key = null) { |
|
419 | 80 | $this_key = !is_null($key) ? $key : self::get_real_key_for_message($message); |
|
420 | |||
421 | 80 | if (!isset($this->_key_to_file[$this_key])) { |
|
422 | // Нет такого файла в списке |
||
423 | 40 | return false; |
|
424 | } |
||
425 | 80 | $filename = $this->_key_to_file[$this_key]; |
|
426 | 80 | if (!file_exists($filename)) { |
|
427 | return false; |
||
428 | } |
||
429 | 80 | if (!is_writable($filename)) { |
|
430 | throw new QueueException('Can not update read only file'); |
||
431 | } |
||
432 | |||
433 | 80 | $fo = fopen($filename, 'r'); |
|
434 | 80 | $locked = flock($fo, LOCK_EX); |
|
435 | 80 | if (!$locked) { |
|
436 | fclose($fo); |
||
437 | throw new QueueException('Can not delete file '.$filename.': '.FileMutex::get_last_php_error_as_string()); |
||
438 | } |
||
439 | 80 | $buf = file_get_contents($filename); |
|
440 | 80 | if (empty($buf)) { |
|
441 | flock($fo, LOCK_UN); |
||
442 | fclose($fo); |
||
443 | throw new QueueException('File "'.$filename.'" is empty'); |
||
444 | } |
||
445 | // @todo обрабатывать ошибки |
||
446 | /** |
||
447 | * @var SmallFilesQueueSingleFile $data_in |
||
448 | */ |
||
449 | 80 | $data_in = Queue::unserialize($buf, $is_valid); |
|
450 | 80 | if (!is_object($data_in)) { |
|
451 | flock($fo, LOCK_UN); |
||
452 | fclose($fo); |
||
453 | throw new QueueException('File "'.$filename.'" does not contain Single Queue object'); |
||
454 | } |
||
455 | 80 | if ($data_in->version != self::SINGLE_FILE_DB_VERSION) { |
|
456 | flock($fo, LOCK_UN); |
||
457 | fclose($fo); |
||
458 | throw new QueueException('Single DB File version mismatch ('.$data_in->version. |
||
459 | ' instead of '.self::SINGLE_FILE_DB_VERSION.')'); |
||
460 | } |
||
461 | 80 | if ($data_in->queue_name != $this->get_queue_name()) { |
|
462 | flock($fo, LOCK_UN); |
||
463 | fclose($fo); |
||
464 | throw new QueueException('Invalid queue name ("'.$data_in->queue_name. |
||
465 | '" instead of "'.$this->get_queue_name().'")'); |
||
466 | } |
||
467 | 80 | $data_in->time_last_update = microtime(true); |
|
468 | |||
469 | // Ищем то же сообщение и заменяем его |
||
470 | 80 | $exists = $this->change_message_in_array($data_in->queue, $message, $this_key); |
|
471 | 80 | if ($exists) { |
|
472 | 80 | $u = file_put_contents($filename, Queue::serialize($data_in)); |
|
473 | 80 | if ($u === false) { |
|
474 | // @codeCoverageIgnoreStart |
||
475 | throw new QueueException('Can not save single query file "'.$filename.'": '. |
||
476 | FileMutex::get_last_php_error_as_string()); |
||
477 | // @codeCoverageIgnoreEnd |
||
478 | } |
||
479 | } |
||
480 | 80 | flock($fo, LOCK_UN); |
|
481 | 80 | fclose($fo); |
|
482 | |||
483 | 80 | return $exists; |
|
484 | } |
||
485 | |||
486 | /** |
||
487 | * Поддерживает ли очередь сортировку event'ов при вставке |
||
488 | * |
||
489 | * @return boolean |
||
490 | */ |
||
491 | 1 | static function is_support_sorted_events() { |
|
494 | |||
495 | /* |
||
496 | * @param double|integer $wait_time |
||
497 | * |
||
498 | * @return iMessage|object|null |
||
499 | * |
||
500 | * @throws QueueException |
||
501 | * @codeCoverageIgnore |
||
502 | / |
||
503 | protected function consume_next_message_with_inotify($wait_time = -1) { |
||
504 | $start = microtime(true); |
||
505 | // @todo написать |
||
506 | throw new QueueException('Не готово'); |
||
507 | } |
||
508 | */ |
||
509 | |||
510 | /* |
||
511 | * @codeCoverageIgnore |
||
512 | / |
||
513 | function test_inotify() { |
||
514 | // @todo этого тут быть не должно |
||
515 | $a = inotify_init(); |
||
516 | } |
||
517 | */ |
||
518 | |||
519 | /** |
||
520 | * @param SmallFilesQueueTransport $queue |
||
521 | * |
||
522 | * @return boolean |
||
523 | */ |
||
524 | 71 | function is_equal_to($queue) { |
|
535 | } |
||
536 | |||
537 | ?> |
If you access a property on an interface, you most likely code against a concrete implementation of the interface.
Available Fixes
Adding an additional type check:
Changing the type hint: