Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like CsvExportService 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 CsvExportService, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
35 | class CsvExportService |
||
36 | { |
||
37 | /** |
||
38 | * @var resource |
||
39 | */ |
||
40 | protected $fp; |
||
41 | |||
42 | /** |
||
43 | * @var boolean |
||
44 | */ |
||
45 | protected $closed = false; |
||
46 | |||
47 | /** |
||
48 | * @var \Closure |
||
49 | */ |
||
50 | protected $convertEncodingCallBack; |
||
51 | |||
52 | /** |
||
53 | * @var EntityManagerInterface |
||
54 | */ |
||
55 | protected $entityManager; |
||
56 | |||
57 | /** |
||
58 | * @var QueryBuilder; |
||
59 | */ |
||
60 | protected $qb; |
||
61 | |||
62 | /** |
||
63 | * @var EccubeConfig |
||
64 | */ |
||
65 | protected $eccubeConfig; |
||
66 | |||
67 | /** |
||
68 | * @var CsvType |
||
69 | */ |
||
70 | protected $CsvType; |
||
71 | |||
72 | /** |
||
73 | * @var Csv[] |
||
74 | */ |
||
75 | protected $Csvs; |
||
76 | |||
77 | /** |
||
78 | * @var CsvRepository |
||
79 | */ |
||
80 | protected $csvRepository; |
||
81 | |||
82 | /** |
||
83 | * @var CsvTypeRepository |
||
84 | */ |
||
85 | protected $csvTypeRepository; |
||
86 | |||
87 | /** |
||
88 | * @var OrderRepository |
||
89 | */ |
||
90 | protected $orderRepository; |
||
91 | |||
92 | /** |
||
93 | * @var ShippingRepository |
||
94 | */ |
||
95 | protected $shippingRepository; |
||
96 | |||
97 | /** |
||
98 | * @var CustomerRepository |
||
99 | */ |
||
100 | protected $customerRepository; |
||
101 | |||
102 | /** |
||
103 | * @var ProductRepository |
||
104 | */ |
||
105 | protected $productRepository; |
||
106 | |||
107 | /** |
||
108 | * @var FormFactoryInterface |
||
109 | */ |
||
110 | protected $formFactory; |
||
111 | |||
112 | /** |
||
113 | * CsvExportService constructor. |
||
114 | * |
||
115 | * @param EntityManagerInterface $entityManager |
||
116 | * @param CsvRepository $csvRepository |
||
117 | * @param CsvTypeRepository $csvTypeRepository |
||
118 | * @param OrderRepository $orderRepository |
||
119 | * @param CustomerRepository $customerRepository |
||
120 | * @param EccubeConfig $eccubeConfig |
||
121 | */ |
||
122 | 76 | public function __construct( |
|
123 | EntityManagerInterface $entityManager, |
||
124 | CsvRepository $csvRepository, |
||
125 | CsvTypeRepository $csvTypeRepository, |
||
126 | OrderRepository $orderRepository, |
||
127 | ShippingRepository $shippingRepository, |
||
128 | CustomerRepository $customerRepository, |
||
129 | ProductRepository $productRepository, |
||
130 | EccubeConfig $eccubeConfig, |
||
131 | FormFactoryInterface $formFactory |
||
132 | ) { |
||
133 | 76 | $this->entityManager = $entityManager; |
|
134 | 76 | $this->csvRepository = $csvRepository; |
|
135 | 76 | $this->csvTypeRepository = $csvTypeRepository; |
|
136 | 76 | $this->orderRepository = $orderRepository; |
|
137 | 76 | $this->shippingRepository = $shippingRepository; |
|
138 | 76 | $this->customerRepository = $customerRepository; |
|
139 | 76 | $this->eccubeConfig = $eccubeConfig; |
|
140 | 76 | $this->productRepository = $productRepository; |
|
141 | 76 | $this->formFactory = $formFactory; |
|
142 | } |
||
143 | |||
144 | /** |
||
145 | * @param $config |
||
146 | */ |
||
147 | public function setConfig($config) |
||
148 | { |
||
149 | $this->eccubeConfig = $config; |
||
150 | } |
||
151 | |||
152 | /** |
||
153 | * @param CsvRepository $csvRepository |
||
154 | */ |
||
155 | public function setCsvRepository(CsvRepository $csvRepository) |
||
156 | { |
||
157 | $this->csvRepository = $csvRepository; |
||
158 | } |
||
159 | |||
160 | /** |
||
161 | * @param CsvTypeRepository $csvTypeRepository |
||
162 | */ |
||
163 | public function setCsvTypeRepository(CsvTypeRepository $csvTypeRepository) |
||
164 | { |
||
165 | $this->csvTypeRepository = $csvTypeRepository; |
||
166 | } |
||
167 | |||
168 | /** |
||
169 | * @param OrderRepository $orderRepository |
||
170 | */ |
||
171 | public function setOrderRepository(OrderRepository $orderRepository) |
||
172 | { |
||
173 | $this->orderRepository = $orderRepository; |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * @param CustomerRepository $customerRepository |
||
178 | */ |
||
179 | public function setCustomerRepository(CustomerRepository $customerRepository) |
||
180 | { |
||
181 | $this->customerRepository = $customerRepository; |
||
182 | } |
||
183 | |||
184 | /** |
||
185 | * @param ProductRepository $productRepository |
||
186 | */ |
||
187 | public function setProductRepository(ProductRepository $productRepository) |
||
188 | { |
||
189 | $this->productRepository = $productRepository; |
||
190 | } |
||
191 | |||
192 | /** |
||
193 | * @param EntityManagerInterface $entityManager |
||
194 | */ |
||
195 | public function setEntityManager(EntityManagerInterface $entityManager) |
||
196 | { |
||
197 | $this->entityManager = $entityManager; |
||
198 | } |
||
199 | |||
200 | /** |
||
201 | * @return EntityManagerInterface |
||
202 | */ |
||
203 | public function getEntityManager() |
||
204 | { |
||
205 | return $this->entityManager; |
||
206 | } |
||
207 | |||
208 | /** |
||
209 | * @param QueryBuilder $qb |
||
210 | */ |
||
211 | 4 | public function setExportQueryBuilder(QueryBuilder $qb) |
|
212 | { |
||
213 | 4 | $this->qb = $qb; |
|
214 | } |
||
215 | |||
216 | /** |
||
217 | * Csv種別からServiceの初期化を行う. |
||
218 | * |
||
219 | * @param $CsvType|integer |
||
220 | */ |
||
221 | 5 | public function initCsvType($CsvType) |
|
222 | { |
||
223 | 5 | if ($CsvType instanceof CsvType) { |
|
224 | $this->CsvType = $CsvType; |
||
225 | } else { |
||
226 | 5 | $this->CsvType = $this->csvTypeRepository->find($CsvType); |
|
227 | } |
||
228 | |||
229 | $criteria = [ |
||
230 | 5 | 'CsvType' => $CsvType, |
|
231 | 'enabled' => true, |
||
232 | ]; |
||
233 | $orderBy = [ |
||
234 | 5 | 'sort_no' => 'ASC', |
|
235 | ]; |
||
236 | 5 | $this->Csvs = $this->csvRepository->findBy($criteria, $orderBy); |
|
237 | } |
||
238 | |||
239 | /** |
||
240 | * @return Csv[] |
||
241 | */ |
||
242 | 4 | public function getCsvs() |
|
243 | { |
||
244 | 4 | return $this->Csvs; |
|
245 | } |
||
246 | |||
247 | /** |
||
248 | * ヘッダ行を出力する. |
||
249 | * このメソッドを使う場合は, 事前にinitCsvType($CsvType)で初期化しておく必要がある. |
||
250 | */ |
||
251 | 4 | public function exportHeader() |
|
252 | { |
||
253 | 4 | if (is_null($this->CsvType) || is_null($this->Csvs)) { |
|
254 | throw new \LogicException('init csv type incomplete.'); |
||
255 | } |
||
256 | |||
257 | 4 | $row = []; |
|
258 | 4 | foreach ($this->Csvs as $Csv) { |
|
259 | 4 | $row[] = $Csv->getDispName(); |
|
260 | } |
||
261 | |||
262 | 4 | $this->fopen(); |
|
263 | 4 | $this->fputcsv($row); |
|
264 | 4 | $this->fclose(); |
|
265 | } |
||
266 | |||
267 | /** |
||
268 | * クエリビルダにもとづいてデータ行を出力する. |
||
269 | * このメソッドを使う場合は, 事前にsetExportQueryBuilder($qb)で出力対象のクエリビルダをわたしておく必要がある. |
||
270 | * |
||
271 | * @param \Closure $closure |
||
272 | */ |
||
273 | 4 | public function exportData(\Closure $closure) |
|
274 | { |
||
275 | 4 | if (is_null($this->qb) || is_null($this->entityManager)) { |
|
276 | throw new \LogicException('query builder not set.'); |
||
277 | } |
||
278 | |||
279 | 4 | $this->fopen(); |
|
280 | |||
281 | 4 | $query = $this->qb->getQuery(); |
|
282 | 4 | foreach ($query->getResult() as $iteratableResult) { |
|
283 | 4 | $closure($iteratableResult, $this); |
|
284 | 4 | $this->entityManager->detach($iteratableResult); |
|
285 | 4 | $query->free(); |
|
286 | 4 | flush(); |
|
287 | } |
||
288 | |||
289 | 4 | $this->fclose(); |
|
290 | } |
||
291 | |||
292 | /** |
||
293 | * CSV出力項目と比較し, 合致するデータを返す. |
||
294 | * |
||
295 | * @param \Eccube\Entity\Csv $Csv |
||
296 | * @param $entity |
||
297 | * |
||
298 | * @return string|null |
||
299 | */ |
||
300 | 4 | public function getData(Csv $Csv, $entity) |
|
301 | { |
||
302 | // エンティティ名が一致するかどうかチェック. |
||
303 | 4 | $csvEntityName = str_replace('\\\\', '\\', $Csv->getEntityName()); |
|
304 | 4 | $entityName = ClassUtils::getClass($entity); |
|
305 | 4 | if ($csvEntityName !== $entityName) { |
|
306 | 2 | return null; |
|
307 | } |
||
308 | |||
309 | // カラム名がエンティティに存在するかどうかをチェック. |
||
310 | 4 | if (!$entity->offsetExists($Csv->getFieldName())) { |
|
311 | 2 | return null; |
|
312 | } |
||
313 | |||
314 | // データを取得. |
||
315 | 4 | $data = $entity->offsetGet($Csv->getFieldName()); |
|
316 | |||
317 | // one to one の場合は, dtb_csv.referece_field_nameと比較し, 合致する結果を取得する. |
||
318 | 4 | if ($data instanceof \Eccube\Entity\AbstractEntity) { |
|
319 | 4 | if (EntityUtil::isNotEmpty($data)) { |
|
320 | 4 | return $data->offsetGet($Csv->getReferenceFieldName()); |
|
321 | } |
||
322 | 4 | } elseif ($data instanceof \Doctrine\Common\Collections\Collection) { |
|
323 | // one to manyの場合は, カンマ区切りに変換する. |
||
324 | $array = []; |
||
325 | foreach ($data as $elem) { |
||
326 | if (EntityUtil::isNotEmpty($elem)) { |
||
327 | $array[] = $elem->offsetGet($Csv->getReferenceFieldName()); |
||
328 | } |
||
329 | } |
||
330 | |||
331 | return implode($this->eccubeConfig['eccube_csv_export_multidata_separator'], $array); |
||
332 | 4 | } elseif ($data instanceof \DateTime) { |
|
333 | // datetimeの場合は文字列に変換する. |
||
334 | 3 | return $data->format($this->eccubeConfig['eccube_csv_export_date_format']); |
|
335 | } else { |
||
336 | // スカラ値の場合はそのまま. |
||
337 | 4 | return $data; |
|
338 | } |
||
339 | |||
340 | return null; |
||
341 | } |
||
342 | |||
343 | /** |
||
344 | * 文字エンコーディングの変換を行うコールバック関数を返す. |
||
345 | * |
||
346 | * @return \Closure |
||
347 | */ |
||
348 | 5 | public function getConvertEncodhingCallback() |
|
349 | { |
||
350 | 5 | $config = $this->eccubeConfig; |
|
351 | |||
352 | 5 | return function ($value) use ($config) { |
|
353 | 5 | return mb_convert_encoding( |
|
354 | 5 | (string) $value, $config['eccube_csv_export_encoding'], 'UTF-8' |
|
355 | ); |
||
356 | 5 | }; |
|
357 | } |
||
358 | |||
359 | 5 | public function fopen() |
|
360 | { |
||
361 | 5 | if (is_null($this->fp) || $this->closed) { |
|
362 | 3 | $this->fp = fopen('php://output', 'w'); |
|
363 | } |
||
364 | } |
||
365 | |||
366 | /** |
||
367 | * @param $row |
||
368 | */ |
||
369 | 5 | public function fputcsv($row) |
|
370 | { |
||
371 | 5 | if (is_null($this->convertEncodingCallBack)) { |
|
372 | 5 | $this->convertEncodingCallBack = $this->getConvertEncodhingCallback(); |
|
373 | } |
||
374 | |||
375 | 5 | fputcsv($this->fp, array_map($this->convertEncodingCallBack, $row), $this->eccubeConfig['eccube_csv_export_separator']); |
|
376 | } |
||
377 | |||
378 | 5 | public function fclose() |
|
379 | { |
||
380 | 5 | if (!$this->closed) { |
|
381 | 5 | fclose($this->fp); |
|
382 | 5 | $this->closed = true; |
|
383 | } |
||
384 | } |
||
385 | |||
386 | /** |
||
387 | * 受注検索用のクエリビルダを返す. |
||
388 | * |
||
389 | * @param Request $request |
||
390 | * |
||
391 | * @return \Doctrine\ORM\QueryBuilder |
||
392 | */ |
||
393 | 1 | View Code Duplication | public function getOrderQueryBuilder(Request $request) |
|
|||
394 | { |
||
395 | 1 | $session = $request->getSession(); |
|
396 | 1 | $builder = $this->formFactory |
|
397 | 1 | ->createBuilder(SearchOrderType::class); |
|
398 | 1 | $searchForm = $builder->getForm(); |
|
399 | |||
400 | 1 | $viewData = $session->get('eccube.admin.order.search', []); |
|
401 | 1 | $searchData = FormUtil::submitAndGetData($searchForm, $viewData); |
|
402 | |||
403 | // 受注データのクエリビルダを構築. |
||
404 | 1 | $qb = $this->orderRepository |
|
405 | 1 | ->getQueryBuilderBySearchDataForAdmin($searchData); |
|
406 | |||
407 | 1 | return $qb; |
|
408 | } |
||
409 | |||
410 | /** |
||
411 | * 会員検索用のクエリビルダを返す. |
||
412 | * |
||
413 | * @param Request $request |
||
414 | * |
||
415 | * @return \Doctrine\ORM\QueryBuilder |
||
416 | */ |
||
417 | 1 | View Code Duplication | public function getCustomerQueryBuilder(Request $request) |
418 | { |
||
419 | 1 | $session = $request->getSession(); |
|
420 | 1 | $builder = $this->formFactory |
|
421 | 1 | ->createBuilder(SearchProductType::class); |
|
422 | 1 | $searchForm = $builder->getForm(); |
|
423 | |||
424 | 1 | $viewData = $session->get('eccube.admin.customer.search', []); |
|
425 | 1 | $searchData = FormUtil::submitAndGetData($searchForm, $viewData); |
|
426 | |||
427 | // 会員データのクエリビルダを構築. |
||
428 | 1 | $qb = $this->customerRepository |
|
429 | 1 | ->getQueryBuilderBySearchData($searchData); |
|
430 | |||
431 | 1 | return $qb; |
|
432 | } |
||
433 | |||
434 | /** |
||
435 | * 商品検索用のクエリビルダを返す. |
||
436 | * |
||
437 | * @param Request $request |
||
438 | * |
||
439 | * @return \Doctrine\ORM\QueryBuilder |
||
440 | */ |
||
441 | View Code Duplication | public function getProductQueryBuilder(Request $request) |
|
457 | } |
||
458 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.