Determine if block has already been registered - wordpress

In gutenberg/block-editor, how can I check whether I've already registered a block type? Is there a function I can use? Searching through the Block Editor Handbook I couldn't see a function to check this.
An example of what I am trying to do is below:
class My_Block {
public function __construct() {
if ( ! SOME_FUNCTION_block_exists('foo/column') ) {
register_block_type( 'foo/column', my_args );
}
}
}

In WordPress Gutenberg, using JavaScript you can check if a block exists by name with getBlockType(), eg:
JavaScript
import { getBlockType } from '#wordpress/blocks';
import { registerBlockType } from '#wordpress/blocks';
if (!getBlockType('foo/column')) {
registerBlockType('foo/column', {
edit: Edit,
save,
});
}
While the above is probably the prefered way, there is a valid case for checking in PHP if a block is already registered, eg. if you want to add a render callback for a block with server side rendering. While I haven't seen a core function for this, I've found a way it can be done by using the REST API endpoint for block-types to search for the block by namespace/name:
PHP
class My_Block
{
public function __construct()
{
if (! is_block_registered('foo/column')) {
register_block_type('foo/column', $args);
}
}
private function is_block_registered($block_name)
{
// Use REST API to query if block exists by <namespace>/<name>
$route = new WP_REST_Request('GET', '/wp/v2/block-types/' . $block_name);
$request = rest_do_request($route);
if ($request->status == 404) {
// Block is not found/registered
return false;
}
// Block is registered (status is 200)
return true;
}
}

There is the method ´is_registered()´ in the class ´WP_Block_Type_Registry´ (that class handles the registration of blocks). See the docs: https://developer.wordpress.org/reference/classes/wp_block_type_registry/is_registered/
class My_Block {
public function __construct() {
if ( ! WP_Block_Type_Registry::get_instance()->is_registered( 'foo/column' ) ) {
register_block_type( 'foo/column', my_args );
}
}
}

Related

Elementor Theme Builder + WooCommerce - custom display conditions for single product

Trying to extend the Elementor Pro Theme Builder to include custom display conditions for Single Products. Using the docs here: https://developers.elementor.com/docs/theme-conditions/
Can get the custom condition displaying fine as a general condition, but I can't work out how to get it as a sub condition for single products (therefore it's not currently usable on single product page templates).
Here's the code I'm currently working with - any hints greatly appreciated!
(Function names and logic generalised for client privacy)
function custom_function( $conditions_manager ) {
$conditions_manager->get_condition( 'singular' )->register_sub_condition( new \Custom_Condition() );
}
add_action( 'elementor/theme/register_conditions', 'custom_function' );
class Custom_Condition extends \ElementorPro\Modules\ThemeBuilder\Conditions\Condition_Base {
public static function get_type() {
return 'singular';
}
public function get_name() {
return 'custom_name';
}
public function get_label() {
return esc_html__( 'Custom label', 'custom-label' );
}
public function check( $args ) {
$post_id = $args['id'];
if($logic_check == "1") {
$is_purchasable = 'true';
} else {
$is_purchasable = 'false';
}
return $is_purchasable;
}
}

Validate the API key and the city name by the API request

I created a custom module where in the block I display the weather using data from https://openweathermap.org/
Code of this block:
https://phpsandbox.io/n/sweet-forest-1lew-1wmof
Also I have WeatherForm.php file with the form which adds in a configuration city and an API key for which it is necessary to display weather.
I needed to Add form validation:
fields should not be empty
City name should not contain numbers
I did it this way:
public function validateForm(array &$form, FormStateInterface $form_state) {
$pattern = '/[0-9]/';
if (empty($form_state->getValue('weather_city'))) {
$form_state->setErrorByName('weather_city', $this->t('Fields should not be empty'));
}
if (preg_match($pattern, $form_state->getValue('weather_city'))) {
$form_state->setErrorByName('weather_city', $this->t('City name should not contain numbers'));
}
}
But I got these remark after the code review:
Also, will be good to validate the API key and the city name by the API request.
I found an example of how to implement this:
public function validateWeatherData(string $city_name, $api_key):bool {
try {
$url = "https://api.openweather.org/data/2.5/weather?q=$city_name&appid=$api_key";
$response = $this->client->request('GET', $url);
if ($response->getStatusCode() != 200) {
throw new \Exception('Failed to retrieve data.');
}
$reg_ex = "#^[A-Za-z-]=$#";
return preg_match($reg_ex, $city_name);
}
catch (GuzzleException $e) {
return FALSE;
}
}
But I don't know how to integrate the example code into my function validateForm. What my code should look like so that it also implements validate the API key and the city name by the API request?
All code of my Form:
https://phpsandbox.io/n/spring-mountain-gdnn-emozx
Why not use both, brainstorm with me something along the lines of..
function validateWeatherData($city, $apikey) {
try {
$url = "https://api.openweather.org/data/2.5/weather?q=$city_name&appid=$api_key"; // Build the URL
$response = file_get_contents($url); // You can use cURL here as well incase CORS blocks file_get_contents
return $response; // Return the data from the call made above
}
catch (Exception $e) {
return $e;
}
}
function validateForm(array &$form, FormStateInterface $form_state) {
$pattern = '/[0-9]/';
if (empty($form_state->getValue('weather_city'))) {
$form_state->setErrorByName('weather_city', $this->t('Fields should not be empty'));
return false; // Failed to validate city, return false go back start again
}
if (preg_match($pattern, $form_state->getValue('weather_city'))) {
$form_state->setErrorByName('weather_city', $this->t('City name should not contain numbers'));
return false; // Failed to validate city, return false go back start again
}
$apikey = "ABCDEFG"; // API key (can be conditional based on city via CASE/IF when needed)
$weatherdata = validateWeatherData($form_state->getValue('weather_city'), $apikey); // Validate weather data
return $weatherdata; // Return validateWeatherData's response or do something else with it
}

Symfony 3, detect browser language

I use Symfony 3.
My website is in 2 languages, French and English and people can switch via a select form.
Default language is French.
Main URL are:
example.com/fr for French version and example.com/en for English version
Well, now, I will like when the user arrives to the website to detect his browser language and redirect to the correct language automatically.
Exemple, if the browser is in French, he is redirected to the French version : example.com/fr
Else he is redirected to the English version: example.com/en
Is there a way to do that properly?
Thank you for your help
If you don't want to rely on other bundles like JMSI18nRoutingBundle
you have to make yourself familiar with Symfony's Event system, e.g. by reading up on the HttpKernel.
For your case you want to hook into the kernel.request event.
Typical Purposes: To add more information to the Request, initialize parts of the system, or return a Response if possible (e.g. a security layer that denies access).
In your custom EventListener you can listen to that event add information to the Request-object used in your router. It could look something like this:
class LanguageListener implements EventSubscriberInterface
{
private $supportedLanguages;
public function __construct(array $supportedLanguages)
{
if (empty($supportedLanguages)) {
throw new \InvalidArgumentException('At least one supported language must be given.');
}
$this->supportedLanguages = $supportedLanguages;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['redirectToLocalizedHomepage', 100],
];
}
public function redirectToLocalizedHomepage(GetResponseEvent $event)
{
// Do not modify sub-requests
if (KernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
// Assume all routes except the frontpage use the _locale parameter
if ($event->getRequest()->getPathInfo() !== '/') {
return;
}
$language = $this->supportedLanguages[0];
if (null !== $acceptLanguage = $event->getRequest()->headers->get('Accept-Language')) {
$negotiator = new LanguageNegotiator();
$best = $negotiator->getBest(
$event->getRequest()->headers->get('Accept-Language'),
$this->supportedLanguages
);
if (null !== $best) {
$language = $best->getType();
}
}
$response = new RedirectResponse('/' . $language);
$event->setResponse($response);
}
}
This listener will check the Accept-Language header of the request and use the Negotiation\LanguageNegotiator to determine the best locale. Be careful as I didn't add the use statements, but they should be fairly obvious.
For a more advanced version you can just read the source for the LocaleChoosingListener from JMSI18nRoutingBundle.
Doing this is usually only required for the frontpage, which is why both the example I posted and the one from the JMSBundle exclude all other paths. For those you can just use the special parameter _locale as described in the documentation:
https://symfony.com/doc/current/translation/locale.html#the-locale-and-the-url
The Symfony documentation also contains an example how to read the locale and make it sticky in a session using a Listener: https://symfony.com/doc/current/session/locale_sticky_session.html
This example also shows how to register the Listener in your services.yml.
Slight changes to #dbrumann's answer to work with my use case and setup:
List of available locales are defined in services.yml file:
parameters:
available_locales:
- nl
- en
- cs
I wanted to determine the locale on any landing page of the website. In case the parsing fails, it fallbacks to _locale parameter or the default one.
class LocaleDetermineSubscriber implements EventSubscriberInterface
{
private $defaultLocale;
private $parameterBag;
private $logger;
public function __construct(ParameterBagInterface $parameterBag,
LoggerInterface $logger,
$defaultLocale = 'en')
{
$this->defaultLocale = $defaultLocale;
$this->parameterBag = $parameterBag;
$this->logger = $logger;
}
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
//do this on first request only
if ($request->hasPreviousSession()) {
return;
}
$allowedLocales = $this->parameterBag->get('available_locales'); //defined in services.yml
$determinedLocale = null;
// use locale from the user preference header
$acceptLanguage = $event->getRequest()->headers->get('Accept-Language');
if ($acceptLanguage != null) {
$negotiator = new LanguageNegotiator();
try {
$best = $negotiator->getBest($acceptLanguage, $allowedLocales);
if ($best != null) {
$language = $best->getType();
$request->setLocale($language);
$determinedLocale = $language;
}
} catch (Exception $e) {
$this->logger->warning("Failed to determine language from Accept-Language header " . $e);
}
}
//check if locale is set with _locale parameter if user preference header parsing not happened
if($determinedLocale == null) {
if ($locale = $request->attributes->get('_locale')) {
if(in_array($locale, $allowedLocales)) {
$request->getSession()->set('_locale', $locale);
} else {
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
} else {
//fallback to default
$request->setLocale($this->defaultLocale);
}
}
}
public static function getSubscribedEvents()
{
return [
// must be registered before (i.e. with a higher priority than) the default Locale listener
KernelEvents::REQUEST => [['onKernelRequest', 25]],
];
}
}
It uses the willdurand/negotiation package, so it needs to be installed first:
composer require willdurand/negotiation
https://packagist.org/packages/willdurand/negotiation

Whats the best way to override object deletion in Sonata Admin?

I already have a custom CRUD controller. So do I just need to override Controller::deleteAction() and Controller::batchDeleteAction() from Sonata\AdminBundle\Controller ?
Or is it preferable / better practice to override the Admin class's delete methods?
My desired behaviour is that I want to update a record with an archived flag rather than delete the entity.
The docs are incomplete on this subject
Update
The following code in my Entity's Repository class iterates over a query object as per the batchDelete method in the ModelManager
public function batchArchive($class, ProxyQuery $queryProxy)
{
$queryProxy->select('DISTINCT '.$queryProxy->getRootAlias());
try {
$entityManager = $this->getEntityManager();
$batchSize = 20;
$i = 0;
foreach ($queryProxy->getQuery()->iterate() as $pos => $object) {
$this->archiveMyEntity($object); //???
if (($i % $batchSize) == 0) {
$entityManager->flush();
$entityManager->clear();
}
++$i;
}
} catch (\PDOException $e) {
throw new ModelManagerException('', 0, $e);
} catch (DBALException $e) {
throw new ModelManagerException('', 0, $e);
}
}
The problem I have is that the object my archiveMyEntity() method expects is an Entity object not a query object.
I overwrote the delete logic in the admin class than in my custom CRUD controller I overwrote the batchActionDelete logic with the following:
public function batchActionDelete(\Sonata\AdminBundle\Datagrid\ProxyQueryInterface $query)
{
if (false === $this->admin->isGranted('DELETE')) {
throw new AccessDeniedException();
}
$res = $query->execute();
if (count($res)) {
foreach ($res as $sqTeamEntity) {
$this->admin->delete($sqTeamEntity, false);
}
$this->admin->flushDoctrine(); //custom method in the admin class
$this->addFlash('sonata_flash_success', 'flash_batch_delete_success');
}
return new RedirectResponse(
$this->admin->generateUrl('list',
$this->admin->getFilterParameters())
);
}
So I fetch all the entities and just call the delete method from the admin class.
Hope this helps.
If you wish to override the controller logic or view, overriding the methods you indicated is the way to go.
However, if your objective is to perform custom logic before or after the deletion, you may override the Admin::preRemove($object) or Admin::postRemove($object) methods.
You may as well override the whole logic by overriding the Admin::delete($object) method.
Feel free to submit a PR - or comment in the related Github issue - to improve the docs as well.

Laravel 4 Model Events don't work with PHPUnit

I build a model side validation in Laravel 4 with the creating Model Event :
class User extends Eloquent {
public function isValid()
{
return Validator::make($this->toArray(), array('name' => 'required'))->passes();
}
public static function boot()
{
parent::boot();
static::creating(function($user)
{
echo "Hello";
if (!$user->isValid()) return false;
});
}
}
It works well but I have issues with PHPUnit. The two following tests are exactly the same but juste the first one pass :
class UserTest extends TestCase {
public function testSaveUserWithoutName()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // pass
assertEquals($count, User::all()->count()); // pass
}
public function testSaveUserWithoutNameBis()
{
$count = User::all()->count();
$user = new User;
$saving = $user->save();
assertFalse($saving); // fail
assertEquals($count, User::all()->count()); // fail, the user is created
}
}
If I try to create a user twice in the same test, it works, but it's like if the binding event is present only in the first test of my test class. The echo "Hello"; is printed only one time, during the first test execution.
I simplify the case for my question but you can see the problem : I can't test several validation rules in different unit tests. I try almost everything since hours but I'm near to jump out the windows now ! Any idea ?
The issue is well documented in Github. See comments above that explains it further.
I've modified one of the 'solutions' in Github to automatically reset all model events during the tests. Add the following to your TestCase.php file.
app/tests/TestCase.php
public function setUp()
{
parent::setUp();
$this->resetEvents();
}
private function resetEvents()
{
// Get all models in the Model directory
$pathToModels = '/app/models'; // <- Change this to your model directory
$files = File::files($pathToModels);
// Remove the directory name and the .php from the filename
$files = str_replace($pathToModels.'/', '', $files);
$files = str_replace('.php', '', $files);
// Remove "BaseModel" as we dont want to boot that moodel
if(($key = array_search('BaseModel', $files)) !== false) {
unset($files[$key]);
}
// Reset each model event listeners.
foreach ($files as $model) {
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Reregister them.
call_user_func(array($model, 'boot'));
}
}
I have my models in subdirectories so I edited #TheShiftExchange code a bit
//Get all models in the Model directory
$pathToModels = '/path/to/app/models';
$files = File::allFiles($pathToModels);
foreach ($files as $file) {
$fileName = $file->getFileName();
if (!ends_with($fileName, 'Search.php') && !starts_with($fileName, 'Base')) {
$model = str_replace('.php', '', $fileName);
// Flush any existing listeners.
call_user_func(array($model, 'flushEventListeners'));
// Re-register them.
call_user_func(array($model, 'boot'));
}
}

Resources