Complex classes like UpdateSimpleProductPage 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 UpdateSimpleProductPage, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
27 | class UpdateSimpleProductPage extends BaseUpdatePage implements UpdateSimpleProductPageInterface |
||
28 | { |
||
29 | use ChecksCodeImmutability; |
||
30 | |||
31 | /** |
||
32 | * {@inheritdoc} |
||
33 | */ |
||
34 | public function nameItIn($name, $localeCode) |
||
35 | { |
||
36 | $this->activateLanguageTab($localeCode); |
||
37 | $this->getElement('name', ['%locale%' => $localeCode])->setValue($name); |
||
38 | |||
39 | $this->waitForSlugGenerationIfNecessary($localeCode); |
||
40 | } |
||
41 | |||
42 | /** |
||
43 | * {@inheritdoc} |
||
44 | */ |
||
45 | public function specifyPrice($channelName, $price) |
||
46 | { |
||
47 | $this->getElement('price', ['%channel%' => $channelName])->setValue($price); |
||
48 | } |
||
49 | |||
50 | public function addSelectedAttributes() |
||
51 | { |
||
52 | $this->clickTabIfItsNotActive('attributes'); |
||
53 | $this->getDocument()->pressButton('Add attributes'); |
||
54 | |||
55 | $form = $this->getDocument()->find('css', 'form'); |
||
56 | |||
57 | $this->getDocument()->waitFor(1, function () use ($form) { |
||
58 | return $form->hasClass('loading'); |
||
59 | }); |
||
60 | } |
||
61 | |||
62 | /** |
||
63 | * {@inheritdoc} |
||
64 | */ |
||
65 | public function removeAttribute($attributeName, $localeCode) |
||
66 | { |
||
67 | $this->clickTabIfItsNotActive('attributes'); |
||
68 | |||
69 | $this->getElement('attribute_delete_button', ['%attributeName%' => $attributeName, '$localeCode%' => $localeCode])->press(); |
||
70 | } |
||
71 | |||
72 | /** |
||
73 | * {@inheritdoc} |
||
74 | */ |
||
75 | public function getAttributeValue($attribute, $localeCode) |
||
76 | { |
||
77 | $this->clickTabIfItsNotActive('attributes'); |
||
78 | $this->clickLocaleTabIfItsNotActive($localeCode); |
||
79 | |||
80 | return $this->getElement('attribute', ['%attributeName%' => $attribute, '%localeCode%' => $localeCode])->getValue(); |
||
81 | } |
||
82 | |||
83 | /** |
||
84 | * {@inheritdoc} |
||
85 | */ |
||
86 | public function getNumberOfAttributes() |
||
87 | { |
||
88 | return count($this->getDocument()->findAll('css', '.attribute')); |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * {@inheritdoc} |
||
93 | */ |
||
94 | public function hasAttribute($attributeName) |
||
95 | { |
||
96 | return null !== $this->getDocument()->find('css', sprintf('.attribute .label:contains("%s")', $attributeName)); |
||
97 | } |
||
98 | |||
99 | /** |
||
100 | * {@inheritdoc} |
||
101 | */ |
||
102 | public function selectMainTaxon(TaxonInterface $taxon) |
||
103 | { |
||
104 | $this->openTaxonBookmarks(); |
||
105 | |||
106 | Assert::isInstanceOf($this->getDriver(), Selenium2Driver::class); |
||
107 | |||
108 | $this->getDriver()->executeScript(sprintf('$(\'input.search\').val(\'%s\')', $taxon->getName())); |
||
109 | $this->getElement('search')->click(); |
||
110 | $this->getElement('search')->waitFor(10, |
||
111 | function () { |
||
112 | return $this->hasElement('search_item_selected'); |
||
113 | }); |
||
114 | $itemSelected = $this->getElement('search_item_selected'); |
||
115 | $itemSelected->click(); |
||
116 | } |
||
117 | |||
118 | /** |
||
119 | * {@inheritdoc} |
||
120 | */ |
||
121 | public function isMainTaxonChosen($taxonName) |
||
122 | { |
||
123 | $this->openTaxonBookmarks(); |
||
124 | |||
125 | return $taxonName === $this->getDocument()->find('css', '.search > .text')->getText(); |
||
126 | } |
||
127 | |||
128 | public function disableTracking() |
||
129 | { |
||
130 | $this->getElement('tracked')->uncheck(); |
||
131 | } |
||
132 | |||
133 | public function enableTracking() |
||
134 | { |
||
135 | $this->getElement('tracked')->check(); |
||
136 | } |
||
137 | |||
138 | /** |
||
139 | * {@inheritdoc} |
||
140 | */ |
||
141 | public function isTracked() |
||
142 | { |
||
143 | return $this->getElement('tracked')->isChecked(); |
||
144 | } |
||
145 | |||
146 | /** |
||
147 | * {@inheritdoc} |
||
148 | */ |
||
149 | public function enableSlugModification($locale) |
||
150 | { |
||
151 | $this->getElement('toggle_slug_modification_button', ['%locale%' => $locale])->press(); |
||
152 | } |
||
153 | |||
154 | /** |
||
155 | * {@inheritdoc} |
||
156 | */ |
||
157 | public function isImageWithTypeDisplayed($type) |
||
158 | { |
||
159 | $imageElement = $this->getImageElementByType($type); |
||
160 | |||
161 | if (null === $imageElement) { |
||
162 | return false; |
||
163 | } |
||
164 | |||
165 | $imageUrl = $imageElement->find('css', 'img')->getAttribute('src'); |
||
166 | $this->getDriver()->visit($imageUrl); |
||
167 | $pageText = $this->getDocument()->getText(); |
||
168 | $this->getDriver()->back(); |
||
169 | |||
170 | return false === stripos($pageText, '404 Not Found'); |
||
171 | } |
||
172 | |||
173 | /** |
||
174 | * {@inheritdoc} |
||
175 | */ |
||
176 | public function attachImage($path, $type = null) |
||
177 | { |
||
178 | $this->clickTabIfItsNotActive('media'); |
||
179 | |||
180 | $filesPath = $this->getParameter('files_path'); |
||
181 | |||
182 | $this->getDocument()->clickLink('Add'); |
||
183 | |||
184 | $imageForm = $this->getLastImageElement(); |
||
185 | if (null !== $type) { |
||
186 | $imageForm->fillField('Type', $type); |
||
187 | } |
||
188 | |||
189 | $imageForm->find('css', 'input[type="file"]')->attachFile($filesPath.$path); |
||
190 | } |
||
191 | |||
192 | /** |
||
193 | * {@inheritdoc} |
||
194 | */ |
||
195 | public function changeImageWithType($type, $path) |
||
196 | { |
||
197 | $filesPath = $this->getParameter('files_path'); |
||
198 | |||
199 | $imageForm = $this->getImageElementByType($type); |
||
200 | $imageForm->find('css', 'input[type="file"]')->attachFile($filesPath.$path); |
||
201 | } |
||
202 | |||
203 | /** |
||
204 | * {@inheritdoc} |
||
205 | */ |
||
206 | public function removeImageWithType($type) |
||
207 | { |
||
208 | $this->clickTabIfItsNotActive('media'); |
||
209 | |||
210 | $imageElement = $this->getImageElementByType($type); |
||
211 | $imageElement->clickLink('Delete'); |
||
212 | } |
||
213 | |||
214 | public function removeFirstImage() |
||
215 | { |
||
216 | $this->clickTabIfItsNotActive('media'); |
||
217 | |||
218 | $imageElement = $this->getFirstImageElement(); |
||
219 | $imageElement->clickLink('Delete'); |
||
220 | } |
||
221 | |||
222 | /** |
||
223 | * {@inheritdoc} |
||
224 | */ |
||
225 | public function modifyFirstImageType($type) |
||
226 | { |
||
227 | $this->clickTabIfItsNotActive('media'); |
||
228 | |||
229 | $firstImage = $this->getFirstImageElement(); |
||
230 | $this->setImageType($firstImage, $type); |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * {@inheritdoc} |
||
235 | */ |
||
236 | public function countImages() |
||
237 | { |
||
238 | $imageElements = $this->getImageElements(); |
||
239 | |||
240 | return count($imageElements); |
||
241 | } |
||
242 | |||
243 | /** |
||
244 | * {@inheritdoc} |
||
245 | */ |
||
246 | public function isSlugReadOnlyIn($locale) |
||
247 | { |
||
248 | return 'readonly' === $this->getElement('slug', ['%locale%' => $locale])->getAttribute('readonly'); |
||
249 | } |
||
250 | |||
251 | /** |
||
252 | * {@inheritdoc} |
||
253 | */ |
||
254 | public function associateProducts(ProductAssociationTypeInterface $productAssociationType, array $productsNames) |
||
|
|||
255 | { |
||
256 | $this->clickTab('associations'); |
||
257 | |||
258 | Assert::isInstanceOf($this->getDriver(), Selenium2Driver::class); |
||
259 | |||
260 | $dropdown = $this->getElement('association_dropdown', [ |
||
261 | '%association%' => $productAssociationType->getName() |
||
262 | ]); |
||
263 | $dropdown->click(); |
||
264 | |||
265 | foreach ($productsNames as $productName) { |
||
266 | $dropdown->waitFor(5, function () use ($productName, $productAssociationType) { |
||
267 | return $this->hasElement('association_dropdown_item', [ |
||
268 | '%association%' => $productAssociationType->getName(), |
||
269 | '%item%' => $productName, |
||
270 | ]); |
||
271 | }); |
||
272 | |||
273 | $item = $this->getElement('association_dropdown_item', [ |
||
274 | '%association%' => $productAssociationType->getName(), |
||
275 | '%item%' => $productName, |
||
276 | ]); |
||
277 | $item->click(); |
||
278 | } |
||
279 | } |
||
280 | |||
281 | /** |
||
282 | * {@inheritdoc} |
||
283 | */ |
||
284 | public function hasAssociatedProduct($productName, ProductAssociationTypeInterface $productAssociationType) |
||
285 | { |
||
286 | $this->clickTabIfItsNotActive('associations'); |
||
287 | |||
288 | return $this->hasElement('association_dropdown_item', [ |
||
289 | '%association%' => $productAssociationType->getName(), |
||
290 | '%item%' => $productName, |
||
291 | ]); |
||
292 | } |
||
293 | |||
294 | /** |
||
295 | * {@inheritdoc} |
||
296 | */ |
||
297 | public function removeAssociatedProduct($productName, ProductAssociationTypeInterface $productAssociationType) |
||
298 | { |
||
299 | $this->clickTabIfItsNotActive('associations'); |
||
300 | |||
301 | $item = $this->getElement('association_dropdown_item_selected', [ |
||
302 | '%association%' => $productAssociationType->getName(), |
||
303 | '%item%' => $productName, |
||
304 | ]); |
||
305 | |||
306 | $deleteIcon = $item->find('css', 'i.delete'); |
||
307 | Assert::notNull($deleteIcon); |
||
308 | $deleteIcon->click(); |
||
309 | } |
||
310 | |||
311 | /** |
||
312 | * {@inheritdoc} |
||
313 | */ |
||
314 | public function getPricingConfigurationForChannelAndCurrencyCalculator(ChannelInterface $channel, CurrencyInterface $currency) |
||
315 | { |
||
316 | $priceConfigurationElement = $this->getElement('pricing_configuration'); |
||
317 | $priceElement = $priceConfigurationElement |
||
318 | ->find('css', sprintf('label:contains("%s %s")', $channel->getCode(), $currency->getCode()))->getParent(); |
||
319 | |||
320 | return $priceElement->find('css', 'input')->getValue(); |
||
321 | } |
||
322 | |||
323 | /** |
||
324 | * {@inheritdoc} |
||
325 | */ |
||
326 | public function getSlug($locale) |
||
327 | { |
||
328 | $this->activateLanguageTab($locale); |
||
329 | |||
330 | return $this->getElement('slug', ['%locale%' => $locale])->getValue(); |
||
331 | } |
||
332 | |||
333 | /** |
||
334 | * {@inheritdoc} |
||
335 | */ |
||
336 | public function specifySlugIn($slug, $locale) |
||
337 | { |
||
338 | $this->activateLanguageTab($locale); |
||
339 | |||
340 | $this->getElement('slug', ['%locale%' => $locale])->setValue($slug); |
||
341 | } |
||
342 | |||
343 | /** |
||
344 | * {@inheritdoc} |
||
345 | */ |
||
346 | public function activateLanguageTab($locale) |
||
347 | { |
||
348 | if (!$this->getDriver() instanceof Selenium2Driver) { |
||
349 | return; |
||
350 | } |
||
351 | |||
352 | $languageTabTitle = $this->getElement('language_tab', ['%locale%' => $locale]); |
||
353 | if (!$languageTabTitle->hasClass('active')) { |
||
354 | $languageTabTitle->click(); |
||
355 | } |
||
356 | } |
||
357 | |||
358 | public function getPriceForChannel($channelName) |
||
359 | { |
||
360 | return $this->getElement('price', ['%channel%' => $channelName])->getValue(); |
||
361 | } |
||
362 | |||
363 | /** |
||
364 | * {@inheritdoc} |
||
365 | */ |
||
366 | protected function getCodeElement() |
||
367 | { |
||
368 | return $this->getElement('code'); |
||
369 | } |
||
370 | |||
371 | /** |
||
372 | * {@inheritdoc} |
||
373 | */ |
||
374 | protected function getElement($name, array $parameters = []) |
||
382 | |||
383 | /** |
||
384 | * {@inheritdoc} |
||
385 | */ |
||
386 | protected function getDefinedElements() |
||
387 | { |
||
388 | return array_merge(parent::getDefinedElements(), [ |
||
389 | 'association_dropdown' => '.field > label:contains("%association%") ~ .product-select', |
||
390 | 'association_dropdown_item' => '.field > label:contains("%association%") ~ .product-select > div.menu > div.item:contains("%item%")', |
||
391 | 'association_dropdown_item_selected' => '.field > label:contains("%association%") ~ .product-select > a.label:contains("%item%")', |
||
392 | 'attribute' => '.tab[data-tab="%localeCode%"] .attribute .label:contains("%attributeName%") ~ input', |
||
393 | 'attribute_delete_button' => '.tab[data-tab="%localeCode%"] .attribute .label:contains("%attributeName%") ~ button', |
||
394 | 'code' => '#sylius_product_code', |
||
395 | 'images' => '#sylius_product_images', |
||
396 | 'language_tab' => '[data-locale="%locale%"] .title', |
||
397 | 'locale_tab' => '#attributesContainer .menu [data-tab="%localeCode%"]', |
||
398 | 'name' => '#sylius_product_translations_%locale%_name', |
||
399 | 'price' => '#sylius_product_variant_channelPricings [data-form-collection="item"]:contains("%channel%") input', |
||
400 | 'pricing_configuration' => '#sylius_calculator_container', |
||
401 | 'search' => '.ui.fluid.search.selection.dropdown', |
||
402 | 'search_item_selected' => 'div.menu > div.item.selected', |
||
403 | 'slug' => '#sylius_product_translations_%locale%_slug', |
||
404 | 'tab' => '.menu [data-tab="%name%"]', |
||
405 | 'taxonomy' => 'a[data-tab="taxonomy"]', |
||
406 | 'tracked' => '#sylius_product_variant_tracked', |
||
407 | 'toggle_slug_modification_button' => '[data-locale="%locale%"] .toggle-product-slug-modification', |
||
408 | ]); |
||
409 | } |
||
410 | |||
411 | private function openTaxonBookmarks() |
||
412 | { |
||
413 | $this->getElement('taxonomy')->click(); |
||
415 | |||
416 | /** |
||
417 | * @param string $tabName |
||
418 | */ |
||
419 | private function clickTabIfItsNotActive($tabName) |
||
426 | |||
427 | /** |
||
428 | * @param string $tabName |
||
429 | */ |
||
430 | private function clickTab($tabName) |
||
435 | |||
436 | /** |
||
437 | * @param string $localeCode |
||
438 | */ |
||
439 | private function clickLocaleTabIfItsNotActive($localeCode) |
||
446 | |||
447 | /** |
||
448 | * @param string $type |
||
449 | * |
||
450 | * @return NodeElement |
||
451 | */ |
||
452 | private function getImageElementByType($type) |
||
463 | |||
464 | /** |
||
465 | * @return NodeElement[] |
||
466 | */ |
||
467 | private function getImageElements() |
||
473 | |||
474 | /** |
||
475 | * @return NodeElement |
||
476 | */ |
||
477 | private function getLastImageElement() |
||
485 | |||
486 | /** |
||
487 | * @return NodeElement |
||
488 | */ |
||
489 | private function getFirstImageElement() |
||
497 | |||
498 | /** |
||
499 | * @param string $locale |
||
500 | */ |
||
501 | private function waitForSlugGenerationIfNecessary($locale) |
||
517 | |||
518 | /** |
||
519 | * @param NodeElement $imageElement |
||
520 | * @param string $type |
||
521 | */ |
||
522 | private function setImageType(NodeElement $imageElement, $type) |
||
527 | } |
||
528 |
Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.