1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
/* |
6
|
|
|
* This file is part of the Sonata Project package. |
7
|
|
|
* |
8
|
|
|
* (c) Thomas Rabaix <[email protected]> |
9
|
|
|
* |
10
|
|
|
* For the full copyright and license information, please view the LICENSE |
11
|
|
|
* file that was distributed with this source code. |
12
|
|
|
*/ |
13
|
|
|
|
14
|
|
|
namespace Sonata\SeoBundle\Block\Social; |
15
|
|
|
|
16
|
|
|
use GuzzleHttp\Exception\GuzzleException; |
17
|
|
|
use Psr\Http\Client\ClientExceptionInterface; |
18
|
|
|
use Psr\Http\Client\ClientInterface; |
19
|
|
|
use Psr\Http\Message\RequestFactoryInterface; |
20
|
|
|
use Sonata\AdminBundle\Form\FormMapper; |
21
|
|
|
use Sonata\BlockBundle\Block\BlockContextInterface; |
22
|
|
|
use Sonata\BlockBundle\Model\BlockInterface; |
23
|
|
|
use Sonata\CoreBundle\Form\Type\ImmutableArrayType; |
24
|
|
|
use Sonata\CoreBundle\Model\Metadata; |
25
|
|
|
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; |
26
|
|
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; |
27
|
|
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; |
28
|
|
|
use Symfony\Component\Form\Extension\Core\Type\IntegerType; |
29
|
|
|
use Symfony\Component\Form\Extension\Core\Type\TextareaType; |
30
|
|
|
use Symfony\Component\Form\Extension\Core\Type\TextType; |
31
|
|
|
use Symfony\Component\HttpFoundation\Response; |
32
|
|
|
use Symfony\Component\OptionsResolver\OptionsResolver; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* This block service allows to embed a tweet by requesting the Twitter API. |
36
|
|
|
* |
37
|
|
|
* @see https://dev.twitter.com/docs/api/1/get/statuses/oembed |
38
|
|
|
* |
39
|
|
|
* @author Hugo Briand <[email protected]> |
40
|
|
|
*/ |
41
|
|
|
class TwitterEmbedTweetBlockService extends BaseTwitterButtonBlockService |
42
|
|
|
{ |
43
|
|
|
public const TWITTER_OEMBED_URI = 'https://api.twitter.com/1/statuses/oembed.json'; |
44
|
|
|
private const TWEET_URL_PATTERN = '%^(https://)(www.)?(twitter.com/)(.*)(/status)(es)?(/)([0-9]*)$%i'; |
45
|
|
|
private const TWEET_ID_PATTERN = '%^([0-9]*)$%'; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var ClientInterface|null |
49
|
|
|
*/ |
50
|
|
|
private $httpClient; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* @var RequestFactoryInterface|null |
54
|
|
|
*/ |
55
|
|
|
private $messageFactory; |
56
|
|
|
|
57
|
|
|
public function __construct( |
58
|
|
|
?string $name, |
59
|
|
|
EngineInterface $templating, |
60
|
|
|
ClientInterface $httpClient = null, |
61
|
|
|
RequestFactoryInterface $messageFactory = null |
62
|
|
|
) { |
63
|
|
|
parent::__construct($name, $templating); |
64
|
|
|
|
65
|
|
|
$this->httpClient = $httpClient; |
66
|
|
|
$this->messageFactory = $messageFactory; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
public function execute(BlockContextInterface $blockContext, Response $response = null) |
70
|
|
|
{ |
71
|
|
|
return $this->renderResponse($blockContext->getTemplate(), [ |
72
|
|
|
'block' => $blockContext->getBlock(), |
73
|
|
|
'tweet' => $this->loadTweet($blockContext), |
74
|
|
|
], $response); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
public function configureSettings(OptionsResolver $resolver): void |
78
|
|
|
{ |
79
|
|
|
$resolver->setDefaults([ |
80
|
|
|
'template' => '@SonataSeo/Block/block_twitter_embed.html.twig', |
81
|
|
|
'tweet' => '', |
82
|
|
|
'maxwidth' => null, |
83
|
|
|
'hide_media' => false, |
84
|
|
|
'hide_thread' => false, |
85
|
|
|
'omit_script' => false, |
86
|
|
|
'align' => 'none', |
87
|
|
|
'related' => null, |
88
|
|
|
'lang' => null, |
89
|
|
|
]); |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
public function buildEditForm(FormMapper $form, BlockInterface $block): void |
93
|
|
|
{ |
94
|
|
|
$form->add('settings', ImmutableArrayType::class, [ |
95
|
|
|
'keys' => [ |
96
|
|
|
['tweet', TextareaType::class, [ |
97
|
|
|
'required' => true, |
98
|
|
|
'label' => 'form.label_tweet', |
99
|
|
|
'sonata_help' => 'form.help_tweet', |
100
|
|
|
]], |
101
|
|
|
['maxwidth', IntegerType::class, [ |
102
|
|
|
'required' => false, |
103
|
|
|
'label' => 'form.label_maxwidth', |
104
|
|
|
'sonata_help' => 'form.help_maxwidth', |
105
|
|
|
]], |
106
|
|
|
['hide_media', CheckboxType::class, [ |
107
|
|
|
'required' => false, |
108
|
|
|
'label' => 'form.label_hide_media', |
109
|
|
|
'sonata_help' => 'form.help_hide_media', |
110
|
|
|
]], |
111
|
|
|
['hide_thread', CheckboxType::class, [ |
112
|
|
|
'required' => false, |
113
|
|
|
'label' => 'form.label_hide_thread', |
114
|
|
|
'sonata_help' => 'form.help_hide_thread', |
115
|
|
|
]], |
116
|
|
|
['omit_script', CheckboxType::class, [ |
117
|
|
|
'required' => false, |
118
|
|
|
'label' => 'form.label_omit_script', |
119
|
|
|
'sonata_help' => 'form.help_omit_script', |
120
|
|
|
]], |
121
|
|
|
['align', ChoiceType::class, [ |
122
|
|
|
'required' => false, |
123
|
|
|
'choices' => [ |
124
|
|
|
'left' => 'form.label_align_left', |
125
|
|
|
'right' => 'form.label_align_right', |
126
|
|
|
'center' => 'form.label_align_center', |
127
|
|
|
'none' => 'form.label_align_none', |
128
|
|
|
], |
129
|
|
|
'label' => 'form.label_align', |
130
|
|
|
]], |
131
|
|
|
['related', TextType::class, [ |
132
|
|
|
'required' => false, |
133
|
|
|
'label' => 'form.label_related', |
134
|
|
|
'sonata_help' => 'form.help_related', |
135
|
|
|
]], |
136
|
|
|
['lang', ChoiceType::class, [ |
137
|
|
|
'required' => true, |
138
|
|
|
'choices' => $this->languageList, |
139
|
|
|
'label' => 'form.label_lang', |
140
|
|
|
]], |
141
|
|
|
], |
142
|
|
|
'translation_domain' => 'SonataSeoBundle', |
143
|
|
|
]); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
public function getBlockMetadata($code = null) |
147
|
|
|
{ |
148
|
|
|
return new Metadata($this->getName(), (null !== $code ? $code : $this->getName()), false, 'SonataSeoBundle', [ |
|
|
|
|
149
|
|
|
'class' => 'fa fa-twitter', |
150
|
|
|
]); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Returns supported API parameters from settings. |
155
|
|
|
* |
156
|
|
|
* @return array |
157
|
|
|
*/ |
158
|
|
|
protected function getSupportedApiParams() |
159
|
|
|
{ |
160
|
|
|
return [ |
161
|
|
|
'maxwidth', |
162
|
|
|
'hide_media', |
163
|
|
|
'hide_thread', |
164
|
|
|
'omit_script', |
165
|
|
|
'align', |
166
|
|
|
'related', |
167
|
|
|
'lang', |
168
|
|
|
'url', |
169
|
|
|
'id', |
170
|
|
|
]; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Builds the API query URI based on $settings. |
175
|
|
|
* |
176
|
|
|
* @param bool $uriMatched |
177
|
|
|
* |
178
|
|
|
* @return string |
179
|
|
|
*/ |
180
|
|
|
protected function buildUri($uriMatched, array $settings) |
181
|
|
|
{ |
182
|
|
|
$apiParams = $settings; |
183
|
|
|
$supportedParams = $this->getSupportedApiParams(); |
184
|
|
|
|
185
|
|
|
if ($uriMatched) { |
186
|
|
|
// We matched the uri |
187
|
|
|
$apiParams['url'] = $settings['tweet']; |
188
|
|
|
} else { |
189
|
|
|
$apiParams['id'] = $settings['tweet']; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
unset($apiParams['tweet']); |
193
|
|
|
|
194
|
|
|
$parameters = []; |
195
|
|
|
foreach ($apiParams as $key => $value) { |
196
|
|
|
if ($value && \in_array($key, $supportedParams, true)) { |
197
|
|
|
$parameters[] = $key.'='.$value; |
198
|
|
|
} |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
return sprintf('%s?%s', self::TWITTER_OEMBED_URI, implode('&', $parameters)); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Loads twitter tweet. |
206
|
|
|
*/ |
207
|
|
|
private function loadTweet(BlockContextInterface $blockContext): ?string |
208
|
|
|
{ |
209
|
|
|
$uriMatched = preg_match(self::TWEET_URL_PATTERN, $blockContext->getSetting('tweet')); |
210
|
|
|
|
211
|
|
|
if (!$uriMatched || !preg_match(self::TWEET_ID_PATTERN, $blockContext->getSetting('tweet'))) { |
212
|
|
|
return null; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
if (null !== $this->httpClient && null !== $this->messageFactory) { |
216
|
|
|
try { |
217
|
|
|
$response = $this->httpClient->sendRequest( |
218
|
|
|
$this->messageFactory->createRequest( |
219
|
|
|
'GET', |
220
|
|
|
$this->buildUri($uriMatched, $blockContext->getSettings()) |
|
|
|
|
221
|
|
|
) |
222
|
|
|
); |
223
|
|
|
} catch (ClientExceptionInterface $e) { |
224
|
|
|
// log error |
225
|
|
|
return null; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
$apiTweet = json_decode($response->getBody(), true); |
229
|
|
|
|
230
|
|
|
return $apiTweet['html']; |
231
|
|
|
} |
232
|
|
|
|
233
|
|
|
// NEXT_MAJOR: Remove the old guzzle implementation |
234
|
|
|
|
235
|
|
|
// We matched an URL or an ID, we'll need to ask the API |
236
|
|
|
if (false === class_exists('GuzzleHttp\Client')) { |
237
|
|
|
throw new \RuntimeException( |
238
|
|
|
'The guzzle http client library is required to call the Twitter API.'. |
239
|
|
|
'Make sure to add psr/http-client or guzzlehttp/guzzle to your composer.json.' |
240
|
|
|
); |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
@trigger_error( |
|
|
|
|
244
|
|
|
'The direct Guzzle implementation is deprecated since 2.x and will be removed with the next major release.', |
245
|
|
|
E_USER_DEPRECATED |
246
|
|
|
); |
247
|
|
|
|
248
|
|
|
// TODO cache API result |
249
|
|
|
$client = new \GuzzleHttp\Client(); |
250
|
|
|
$client->setConfig(['curl.options' => [CURLOPT_CONNECTTIMEOUT_MS => 1000]]); |
251
|
|
|
|
252
|
|
|
try { |
253
|
|
|
$request = $client->get($this->buildUri($uriMatched, $blockContext->getSettings())); |
|
|
|
|
254
|
|
|
$apiTweet = json_decode($request->send()->getBody(true), true); |
|
|
|
|
255
|
|
|
|
256
|
|
|
return $apiTweet['html']; |
257
|
|
|
} catch (GuzzleException $e) { |
258
|
|
|
// log error |
259
|
|
|
return null; |
260
|
|
|
} |
261
|
|
|
// END NEXT_MAJOR |
262
|
|
|
} |
263
|
|
|
} |
264
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.