Passed
Push — master ( f58ad4...2006a7 )
by Florian
16:38 queued 11s
created

AbstractParser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 0
nc 1
nop 3
dl 0
loc 5
ccs 2
cts 2
cp 1
crap 1
rs 10
c 1
b 0
f 0
1
<?php
2
3
namespace App\Parser;
4
5
use App\DataProvider\ProviderProvider;
6
use App\DTO\Property;
7
use App\DTO\PropertyAd;
8
use App\Enum\PropertyFilter;
9
use App\Exception\ParseException;
10
use App\Formatter\DecimalFormatter;
11
use DateTime;
12
use Exception;
13
use Psr\Log\LoggerInterface;
14
use Symfony\Component\DomCrawler\Crawler;
15
use function array_filter;
16
use function Symfony\Component\String\u;
17
18
abstract class AbstractParser implements ParserInterface
19
{
20
    // Redefined in the child classes
21
    protected const PROVIDER = null;
22
23
    protected const SELECTOR_AD_WRAPPER    = null;
24
    protected const SELECTOR_PRICE         = null;
25
    protected const SELECTOR_AREA          = null;
26
    protected const SELECTOR_ROOMS_COUNT   = null;
27
    protected const SELECTOR_LOCATION      = null;
28
    protected const SELECTOR_BUILDING_NAME = null;
29
    protected const SELECTOR_TITLE         = null;
30
    protected const SELECTOR_DESCRIPTION   = null;
31
    protected const SELECTOR_PHOTO         = 'img:first-child';
32
    protected const SELECTOR_URL           = 'a:first-child';
33
34
    private const NEW_BUILD_WORDS = ['neuf', 'livraison', 'programme', 'neuve', 'nouveau', 'nouvelle', 'remise'];
35
36 16
    public function __construct(
37
        private ProviderProvider $providerProvider,
38
        protected DecimalFormatter $formatter,
39
        protected LoggerInterface $logger
40 16
    ) {}
41
42
    /**
43
     * {@inheritDoc}
44
     */
45 16
    public function parse(string $html, array $filters = [], array $params = []): array
46
    {
47 16
        $properties = [];
48
49
        // Iterate over all DOM elements wrapping a property ad
50 16
        ($this->createCrawler($html))->filter(static::SELECTOR_AD_WRAPPER)->each(function (Crawler $node) use (&$properties, $params) {
0 ignored issues
show
Bug introduced by
static::SELECTOR_AD_WRAPPER of type null is incompatible with the type string expected by parameter $selector of Symfony\Component\DomCrawler\Crawler::filter(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

50
        ($this->createCrawler($html))->filter(/** @scrutinizer ignore-type */ static::SELECTOR_AD_WRAPPER)->each(function (Crawler $node) use (&$properties, $params) {
Loading history...
51
            try {
52 16
                $properties[] = $this->parseOne($node, $params['date']);
53
            } catch (Exception $e) {
54
                $this->logger->warning('Error while parsing a property: ' . $e->getMessage(), $params);
55
            }
56 16
        });
57
58 16
        if (empty($properties)) {
59
            throw new ParseException('No property parsed');
60
        }
61
62
        // Filter the properties
63 16
        $properties = array_filter($properties, static fn(Property $ad) => isset($filters[PropertyFilter::NEW_BUILD]) ? $ad->isNewBuild() : true);
64
65 16
        return $properties;
66
    }
67
68
    /**
69
     * Enable to modify the DOM before parsing
70
     *
71
     * @param string $html
72
     *
73
     * @return Crawler
74
     */
75 14
    protected function createCrawler(string $html): Crawler
76
    {
77 14
        return new Crawler($html);
78
    }
79
80 14
    protected function parsePrice(Crawler $crawler): ?float
81
    {
82 14
        if (null === static::SELECTOR_PRICE) {
0 ignored issues
show
introduced by
The condition null === static::SELECTOR_PRICE is always true.
Loading history...
83 13
            return $this->formatter->parsePrice($crawler->html());
84
        }
85
86
        try {
87 1
            $priceStr = trim($crawler->filter(static::SELECTOR_PRICE)->text());
88
        } catch (Exception) {
89
            return null;
90
        }
91
92 1
        return $this->formatter->parsePrice($priceStr);
93
    }
94
95 16
    protected function parseArea(Crawler $crawler): ?float
96
    {
97 16
        if (null === static::SELECTOR_AREA) {
0 ignored issues
show
introduced by
The condition null === static::SELECTOR_AREA is always true.
Loading history...
98 16
            return $this->formatter->parseArea($crawler->html());
99
        }
100
101
        try {
102
            $areaStr = trim($crawler->filter(static::SELECTOR_AREA)->text());
103
        } catch (Exception) {
104
            return null;
105
        }
106
107
        return $this->formatter->parseArea($areaStr);
108
    }
109
110 16
    protected function parseRoomsCount(Crawler $crawler): ?int
111
    {
112 16
        if (null === static::SELECTOR_ROOMS_COUNT) {
0 ignored issues
show
introduced by
The condition null === static::SELECTOR_ROOMS_COUNT is always true.
Loading history...
113 15
            return $this->formatter->parseRoomsCount($crawler->html());
114
        }
115
116
        try {
117 1
            $roomsCountStr = trim($crawler->filter(static::SELECTOR_ROOMS_COUNT)->text());
118
        } catch (Exception) {
119
            return null;
120
        }
121
122 1
        return $this->formatter->parseRoomsCount($roomsCountStr);
123
    }
124
125 14
    protected function parseLocation(Crawler $crawler): ?string
126
    {
127 14
        if (null === static::SELECTOR_LOCATION) {
0 ignored issues
show
introduced by
The condition null === static::SELECTOR_LOCATION is always true.
Loading history...
128 1
            return null;
129
        }
130
131
        try {
132 13
            return trim($crawler->filter(static::SELECTOR_LOCATION)->text());
133
        } catch (Exception) {
134
            return null;
135
        }
136
    }
137
138 16
    protected function parseBuildingName(Crawler $crawler): ?string
139
    {
140 16
        if (null === static::SELECTOR_BUILDING_NAME) {
0 ignored issues
show
introduced by
The condition null === static::SELECTOR_BUILDING_NAME is always true.
Loading history...
141 10
            return null;
142
        }
143
144
        try {
145 6
            return trim($crawler->filter(static::SELECTOR_BUILDING_NAME)->text());
146
        } catch (Exception) {
147
            return null;
148
        }
149
    }
150
151 16
    protected function parseTitle(Crawler $crawler): ?string
152
    {
153 16
        if (null === static::SELECTOR_TITLE) {
0 ignored issues
show
introduced by
The condition null === static::SELECTOR_TITLE is always true.
Loading history...
154 13
            return null;
155
        }
156
157
        try {
158 3
            return trim($crawler->filter(static::SELECTOR_TITLE)->text());
159
        } catch (Exception) {
160
            return null;
161
        }
162
    }
163
164 16
    protected function parseDescription(Crawler $crawler): ?string
165
    {
166 16
        if (null === static::SELECTOR_DESCRIPTION) {
0 ignored issues
show
introduced by
The condition null === static::SELECTOR_DESCRIPTION is always true.
Loading history...
167 9
            return null;
168
        }
169
170
        try {
171 7
            return trim($crawler->filter(static::SELECTOR_DESCRIPTION)->text());
172 1
        } catch (Exception) {
173 1
            return null;
174
        }
175
    }
176
177
    /**
178
     * @throws ParseException
179
     */
180 14
    protected function parsePhoto(Crawler $crawler): ?string
181
    {
182
        try {
183 14
            return $crawler->filter(static::SELECTOR_PHOTO)->attr('src');
184
        } catch (Exception $e) {
185
            throw new ParseException('Error while parsing the photo: ' . $e->getMessage());
186
        }
187
    }
188
189
    /**
190
     * @throws ParseException
191
     */
192 16
    protected function parseUrl(Crawler $crawler): string
193
    {
194
        try {
195 16
            return $crawler->filter(static::SELECTOR_URL)->attr('href');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $crawler->filter(...CTOR_URL)->attr('href') could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
196
        } catch (Exception $e) {
197
            throw new ParseException('Error while parsing the URL: ' . $e->getMessage());
198
        }
199
    }
200
201
    /**
202
     * @throws ParseException
203
     */
204 16
    private function parseOne(Crawler $crawler, DateTime $publishedAt): Property
205
    {
206 16
        $propertyAd = (new PropertyAd)
207 16
            ->setProvider(static::PROVIDER)
0 ignored issues
show
Bug introduced by
static::PROVIDER of type null is incompatible with the type string expected by parameter $provider of App\DTO\PropertyAd::setProvider(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

207
            ->setProvider(/** @scrutinizer ignore-type */ static::PROVIDER)
Loading history...
208 16
            ->setTitle($this->parseTitle($crawler))
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseTitle($crawler) targeting App\Parser\AbstractParser::parseTitle() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
209 16
            ->setDescription($this->parseDescription($crawler))
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseDescription($crawler) targeting App\Parser\AbstractParser::parseDescription() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
210 16
            ->setPhoto($this->parsePhoto($crawler))
0 ignored issues
show
Bug introduced by
It seems like $this->parsePhoto($crawler) can also be of type null; however, parameter $photo of App\DTO\PropertyAd::setPhoto() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

210
            ->setPhoto(/** @scrutinizer ignore-type */ $this->parsePhoto($crawler))
Loading history...
211 16
            ->setUrl($this->parseUrl($crawler))
212 16
            ->setPublishedAt($publishedAt);
213
214 16
        $property = (new Property)
215 16
            ->setPrice($this->parsePrice($crawler))
216 16
            ->setArea($this->parseArea($crawler))
217 16
            ->setRoomsCount($this->parseRoomsCount($crawler))
218 16
            ->setLocation($this->parseLocation($crawler))
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseLocation($crawler) targeting App\Parser\AbstractParser::parseLocation() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
219 16
            ->setBuildingName($this->parseBuildingName($crawler))
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->parseBuildingName($crawler) targeting App\Parser\AbstractParser::parseBuildingName() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
220 16
            ->setAd($propertyAd);
221
222 16
        if ((null !== $provider = $this->providerProvider->find(static::PROVIDER)) && $provider->isNewBuildOnly()) {
0 ignored issues
show
Bug introduced by
static::PROVIDER of type null is incompatible with the type string expected by parameter $name of App\DataProvider\ProviderProvider::find(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

222
        if ((null !== $provider = $this->providerProvider->find(/** @scrutinizer ignore-type */ static::PROVIDER)) && $provider->isNewBuildOnly()) {
Loading history...
223 5
            $property->setNewBuild(true);
224
        } else {
225 11
            $property->setNewBuild(u($propertyAd->getTitle() . $propertyAd->getDescription())->containsAny(self::NEW_BUILD_WORDS));
226
        }
227
228 16
        return $property;
229
    }
230
}
231