Completed
Pull Request — 5.6 (#2830)
by Jeroen
14:14
created

src/Kunstmaan/SeoBundle/Twig/SeoTwigExtension.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Kunstmaan\SeoBundle\Twig;
4
5
use Doctrine\ORM\EntityManager;
6
use Kunstmaan\AdminBundle\Entity\AbstractEntity;
7
use Kunstmaan\NodeBundle\Entity\AbstractPage;
8
use Kunstmaan\SeoBundle\Entity\Seo;
9
use Psr\Cache\CacheItemPoolInterface;
10
use Twig\Environment;
11
use Twig\Extension\AbstractExtension;
12
use Twig\TwigFunction;
13
14
/**
15
 * Twig extensions for Seo
16
 *
17
 * @final since 5.4
18
 */
19
class SeoTwigExtension extends AbstractExtension
20
{
21
    /**
22
     * @var EntityManager
23
     */
24
    protected $em;
25
26
    /**
27
     * Website title defined in your parameters
28
     *
29
     * @var string
30
     */
31
    private $websiteTitle;
32
33
    /**
34
     * Saves querying the db multiple times, if you happen to use any of the defined
35
     * functions more than once in your templates
36
     *
37
     * @var array
38
     */
39
    private $seoCache = [];
40
41
    /**
42
     * @var CacheItemPoolInterface
43
     */
44
    private $requestCache;
45
46
    public function __construct(EntityManager $em)
0 ignored issues
show
You have injected the EntityManager via parameter $em. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

Loading history...
47
    {
48
        $this->em = $em;
49
    }
50
51
    /**
52
     * Returns a list of functions to add to the existing list.
53
     *
54
     * @return array An array of functions
55
     */
56
    public function getFunctions()
57
    {
58
        return [
59
            new TwigFunction('render_seo_metadata_for', [$this, 'renderSeoMetadataFor'], ['is_safe' => ['html'], 'needs_environment' => true]),
60
            new TwigFunction('get_seo_for', [$this, 'getSeoFor']),
61
            new TwigFunction('get_title_for', [$this, 'getTitleFor']),
62
            new TwigFunction('get_title_for_page_or_default', [$this, 'getTitleForPageOrDefault']),
63
            new TwigFunction('get_absolute_url', [$this, 'getAbsoluteUrl']),
64
            new TwigFunction('get_image_dimensions', [$this, 'getImageDimensions']),
65
        ];
66
    }
67
68
    /**
69
     * Validates the $url value as URL (according to » http://www.faqs.org/rfcs/rfc2396), optionally with required components.
70
     * It will just return the url if it's valid. If it starts with '/', the $host will be prepended.
71
     *
72
     * @param string $url
73
     * @param string $host
74
     *
75
     * @return string
76
     */
77
    public function getAbsoluteUrl($url, $host = null)
78
    {
79
        $validUrl = filter_var($url, FILTER_VALIDATE_URL);
80
        $host = rtrim($host, '/');
81
82
        if (!$validUrl === false) {
83
            // The url is valid
84
            return $url;
85
        }
86
87
        // Prepend with $host if $url starts with "/"
88
        if (strpos($url, '/') === 0) {
89
            return $url = $host . $url;
90
        }
91
92
        return false;
93
    }
94
95
    /**
96
     * @return Seo
97
     */
98
    public function getSeoFor(AbstractPage $entity)
99
    {
100
        $key = md5(\get_class($entity) . $entity->getId());
101
102
        if (!\array_key_exists($key, $this->seoCache)) {
103
            $seo = $this->em->getRepository(Seo::class)->findOrCreateFor($entity);
104
            $this->seoCache[$key] = $seo;
105
        }
106
107
        return $this->seoCache[$key];
108
    }
109
110
    /**
111
     * The first value that is not null or empty will be returned.
112
     *
113
     * @param AbstractPage $entity the entity for which you want the page title
114
     *
115
     * @return string The page title. Will look in the SEO meta first, then the NodeTranslation, then the page.
116
     */
117
    public function getTitleFor(AbstractPage $entity)
118
    {
119
        $arr = [];
120
121
        $arr[] = $this->getSeoTitle($entity);
122
123
        $arr[] = $entity->getTitle();
124
125
        return $this->getPreferredValue($arr);
126
    }
127
128
    /**
129
     * @param AbstractPage $entity
0 ignored issues
show
Should the type for parameter $entity not be null|AbstractPage?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
130
     * @param string|null  $default if given we'll return this text if no SEO title was found
131
     *
132
     * @return string
0 ignored issues
show
Should the return type not be string|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
133
     */
134
    public function getTitleForPageOrDefault(AbstractPage $entity = null, $default = null)
135
    {
136
        if (\is_null($entity)) {
137
            return $default;
138
        }
139
140
        $arr = [];
141
142
        $arr[] = $this->getSeoTitle($entity);
143
144
        $arr[] = $default;
145
146
        $arr[] = $entity->getTitle();
147
148
        return $this->getPreferredValue($arr);
149
    }
150
151
    /**
152
     * @param AbstractEntity $entity      The entity
153
     * @param mixed          $currentNode The current node
154
     * @param string         $template    The template
155
     *
156
     * @return string
157
     */
158
    public function renderSeoMetadataFor(Environment $environment, AbstractEntity $entity, $currentNode = null, $template = '@KunstmaanSeo/SeoTwigExtension/metadata.html.twig')
159
    {
160
        $seo = $this->getSeoFor($entity);
0 ignored issues
show
$entity of type object<Kunstmaan\AdminBu...\Entity\AbstractEntity> is not a sub-type of object<Kunstmaan\NodeBundle\Entity\AbstractPage>. It seems like you assume a child class of the class Kunstmaan\AdminBundle\Entity\AbstractEntity to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
161
        $template = $environment->load($template);
162
163
        return $template->render(
164
            [
165
                'seo' => $seo,
166
                'entity' => $entity,
167
                'currentNode' => $currentNode,
168
            ]
169
        );
170
    }
171
172
    /**
173
     * @return string
174
     */
175
    protected function getPreferredValue(array $values)
176
    {
177
        foreach ($values as $v) {
178
            if (!\is_null($v) && !empty($v)) {
179
                return $v;
180
            }
181
        }
182
183
        return '';
184
    }
185
186
    /**
187
     * @param AbstractPage $entity
0 ignored issues
show
Should the type for parameter $entity not be null|AbstractPage?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
188
     *
189
     * @return string|null
190
     */
191
    private function getSeoTitle(AbstractPage $entity = null)
192
    {
193
        if (\is_null($entity)) {
194
            return null;
195
        }
196
197
        $seo = $this->getSeoFor($entity);
198
        if (!\is_null($seo)) {
199
            $title = $seo->getMetaTitle();
200
            if (!empty($title)) {
201
                return str_replace('%websitetitle%', $this->getWebsiteTitle(), $title);
202
            }
203
        }
204
205
        return null;
206
    }
207
208
    /**
209
     * Gets the Website title defined in your parameters.
210
     *
211
     * @return string
212
     */
213
    public function getWebsiteTitle()
214
    {
215
        return $this->websiteTitle;
216
    }
217
218
    /**
219
     * Sets the Website title defined in your parameters.
220
     *
221
     * @param string $websiteTitle the website title
222
     *
223
     * @return self
224
     */
225
    public function setWebsiteTitle($websiteTitle)
226
    {
227
        $this->websiteTitle = $websiteTitle;
228
229
        return $this;
230
    }
231
232
    /**
233
     * @param $src
234
     *
235
     * @return array
236
     */
237
    public function getImageDimensions($src)
238
    {
239
        list($width, $height) = $this->getImageSize($src);
240
241
        return ['width' => $width, 'height' => $height];
242
    }
243
244
    public function setRequestCache(CacheItemPoolInterface $cacheService)
245
    {
246
        $this->requestCache = $cacheService;
247
    }
248
249
    /**
250
     * @return CacheItemPoolInterface
251
     */
252
    public function getRequestCache()
253
    {
254
        return $this->requestCache;
255
    }
256
257
    private function getImageSize($src)
258
    {
259
        try {
260
            $cache = $this->getRequestCache();
261
            if (null === $cache) {
262
                return getimagesize($src);
263
            }
264
265
            $cachedImageSizes = $cache->getItem(md5($src));
266
            if (!$cachedImageSizes->isHit()) {
267
                $sizes = getimagesize($src);
268
269
                $cachedImageSizes->set($sizes);
270
                $cache->save($cachedImageSizes);
271
            }
272
273
            return $cachedImageSizes->get();
274
        } catch (\Exception $e) {
275
            return [null, null];
276
        }
277
    }
278
}
279