dominik

joined 5 months ago
MODERATOR OF
5
submitted 5 months ago* (last edited 4 months ago) by [email protected] to c/[email protected]
 

The problem

When you have a <ng-template> that accepts parameters via context, you usually lose TypeScript's type safety, reverting to the prehistoric age of JavaScript with no type enforcement:

<ng-template #someTemplate let-someVariable="someVariable">
  {{Math.abs(someVariable)}} <!-- compiler and IDE have no idea that the variable is a string -->
</ng-template>

With this approach, you can perform any operation on someVariable, and the compiler won't warn you—even if it results in runtime errors.

The solution

To ensure type safety, we can create a type assertion guard directive:

@Directive({
  selector: 'ng-template[some-template]',
  standalone: true,
})
export class SomeTemplateNgTemplate {
  static ngTemplateContextGuard(
    directive: SomeTemplateNgTemplate,
    context: unknown
  ): context is {someVariable: string} {
    return true;
  }
}

Explanation

  1. Directive setup

    • This directive applies to <ng-template> elements that include the some-template attribute (ng-template[some-template] in the selector).
    • It's marked as standalone, which is the recommended approach in modern Angular.
  2. Type Context Guard

    • The class name is not important and can be anything.

    • The static ngTemplateContextGuard function is where the magic happens.

    • It must accept two parameters:

      • An instance of itself (directive: SomeTemplateNgTemplate).
      • The context (which is typed as unknown which is a more type-safe any).
    • The return type uses a TypeScript type predicate, which tells the compiler: If this function returns true, then the context must match the given type { someVariable: string }.

Since this function always returns true, TypeScript will assume that every template using this directive has the expected type.

Important note: As with all TypeScript type assertions, this is a compile-time safety measure—it does not enforce types at runtime. You can still pass invalid values, but TypeScript will warn you beforehand.

Applying the Directive

Now, update your template to use the directive:

<ng-template some-template #someTemplate let-someVariable="someVariable">
  {{Math.abs(someVariable)}}
</ng-template>

The result

With the some-template directive in place, Angular now correctly infers the type of someVariable. If you try to use Math.abs(someVariable), TypeScript will now show an error:

NG5: Argument of type 'string' is not assignable to parameter of type 'number'.

Conclusion

By leveraging ngTemplateContextGuard, you can enforce strong typing within ng-template contexts, making your Angular code safer and more maintainable. This simple trick helps catch potential errors at compile time rather than at runtime—ensuring better developer experience and fewer unexpected bugs.

6
submitted 5 months ago* (last edited 4 months ago) by [email protected] to c/[email protected]
 

If you're unsure where you could (or why you should) use feature flags in your project, this section is for you, otherwise feel free to skip this part.

What are feature flags

Feature flags are runtime switches that enable or disable specific code paths dynamically. You might already be using them without realizing it! If your system allows enabling or disabling functionality via database settings (e.g., toggling registrations, comments, or user uploads), you're already using a basic form of feature flags. But these self-built options are rarely as thought-out as dedicated feature flagging systems.

Dedicated feature flagging systems

Dedicated feature flagging systems provide a standardized way to manage feature toggles and unlock additional use cases, such as:

  • Gradually roll out features to a subset of users, such as internal users or beta testers.

    • Makes it possible to do a gradual rollout to test out the reactions without deploying a feature to everyone
    • Enable features based on the region of the user (like GDPR, CCPA)
  • Create experimental features without maintaining separate branches

  • A/B test multiple versions of a new feature

  • Implement a kill switch to turn off some parts of the code in case of emergency (attack, data corruption...)

  • Replace your built-in permission system

  • Create toggleable features that are only needed in certain cases (for example, enable a high verbosity logging if you run into issues)

  • Rollback features if they're broken

  • and many more

Unleash

Disclaimer: I originally wrote the open-source Unleash PHP SDK, which was later adopted as the official Unleash SDK. While I’m paid to maintain it, this article is not sponsored (and I'm not an employee of Unleash). I’m writing it for the same reasons I originally created the SDK: I love how Unleash is implemented and think more people should use it!

Unleash is one such system. Unleash offers both a paid plan and a self-hosted open-source version. While the open-source version lacks some premium features, since the release of the constraints feature to the OSS version it's feature-complete for my needs.

What makes Unleash unique is the way the feature evaluation is handled: everything happens locally, meaning your app does not leak any data to Unleash. Your application also avoids performance overhead from unnecessary HTTP requests. Usually these systems do the evaluation on the server and just return a yes/no response. With Unleash, you instead get the whole configuration as a simple JSON and the SDK does evaluation locally (to the point that you could even use the SDK without Unleash at all, you can simply provide a static JSON). Furthermore, the features are cached locally for half a minute or so, thus the only I/O overhead Unleash adds is 2 http requests a minute. And another cool feature is that they support pretty much every major programming language. Now that my fanboying is over, let's go over Unleash in PHP!

Unleash in PHP

Installing the SDK is straightforward, simply run composer require unleash/client. The documentation can be found at Packagist or GitHub. It supports PHP versions as old as 7.2. Afterwards you create an instance of the Unleash object that you will use throughout your code:

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://my-unleash-server.com/api/')
    ->withInstanceId('Some instance id')
    ->build();

The app name and instance ID are used to identify clients. The app URL is the Unleash server endpoint, which you can find in the settings page.

Once you've set up the Unleash object, using it is extremely simple:

if ($unleash->isEnabled('new-product-page')) {
  // do one thing
} else if ($unleash->isEnabled('semi-new-product-page')) {
  // do other thing
} else {
  // do yet another thing
}

If you do A/B testing, you can configure variants like this:

$topMenuVariant = $unleash->getVariant('top-menu');
if (!$topMenuVariant->isEnabled()) {
  // todo the user does not have access to the feature at all
} else {
  $payload = $topMenuVariant->getPayload();
  // let's assume the payload is a JSON
  assert($payload->getType() === VariantPayloadType::JSON);
  $payloadData = $payload->fromJson();

  // todo display the menu based on the received payload
}

Configuring the features

All of the above must be configured somewhere and that place is the Unleash UI. You can test out their official demo (just put whatever email in there, it doesn't even have to be real, there's no confirmation) if you don't want to install Unleash locally.

Each feature has multiple environments, by default a development and production one (I think in the open source version you cannot create more, though I successfully did so by fiddling directly with the database) and each environment must have one or more strategies (unless the environment is disabled). Strategies is what controls whether the feature is enabled for a user or not. I'll go briefly over the simple strategies and then write a bit more about the complex ones (and custom ones).

  1. Standard - simple yes/no strategy, no configuration, just enabled or disabled
  2. User IDs - enable the feature for specific user IDs
  3. IPs and Hosts - enable the feature for specific IP addresses and hostnames respectively

Unleash doesn’t automatically know your app’s user IDs—you need to provide them via an Unleash context:

$context = new UnleashContext(currentUserId: '123');

if ($unleash->isEnabled('some-feature', $context)) {
  // todo
}

Or more likely, if you don't want to pass around a manually created context all the time, just create a provider that will create the default context:

final class MyContextProvider implements UnleashContextProvider
{
    public function getContext(): Context
    {
        $context = new UnleashContext();
        $context->setCurrentUserId('user id from my app');

        return $context;     
    }
}

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://my-unleash-server.com/api/')
    ->withInstanceId('Some instance id')
    ->withContextProvider(new MyContextProvider())
    ->build();

if ($unleash->isEnabled('some-feature')) {
  // todo
}

The Gradual rollout strategy

This powerful strategy allows you to roll out features to a percentage of users based on a chosen context field (e.g., user ID, IP address, or any custom attribute). With the help of constraints you can configure very complex access scenarios thanks to the many operators that are available (various string, array, date, numeric and version operators) for each of your context fields. So in short, you create arbitrary fields in your context which you can then validate with any of the supported operators.

This is sort of becoming the catch-all default strategy because it can do everything the others can with the help of constraints. If you want to emulate the Standard strategy, just make it always available to 100% of your users. Emulating User IDs strategy can be done by having it available to 100% of your userbase and adding a constraint that the userId must be one of the specified values. And so on.

Custom strategies

Need even more flexibility? You can create custom strategies! Here’s a real-world example from one of my projects:

<?php

namespace App\Service\Unleash;

use InvalidArgumentException;
use Unleash\Client\Configuration\Context;
use Unleash\Client\DTO\Strategy;
use Unleash\Client\Strategy\AbstractStrategyHandler;
use Override;

final class AccountIdUnleashStrategy extends AbstractStrategyHandler
{
    public const string CONTEXT_NAME = 'currentAccountId';

    #[Override]
    public function getStrategyName(): string
    {
        return 'accountId';
    }

    #[Override]
    public function isEnabled(Strategy $strategy, Context $context): bool
    {
        $allowedAccountIds = $this->findParameter('accountIds', $strategy);
        if (!$allowedAccountIds) {
            return false;
        }

        try {
            $currentCompanyAccountId = $context->getCustomProperty(self::CONTEXT_NAME);
        } catch (InvalidArgumentException) {
            return false;
        }

        $allowedAccountIds = array_map('trim', explode(',', $allowedAccountIds));
        $enabled = in_array($currentCompanyAccountId, $allowedAccountIds, true);

        if (!$enabled) {
            return false;
        }

        return $this->validateConstraints($strategy, $context);
    }
}

Then simply register it:

$unleash = UnleashBuilder::create()
    ->withAppName('Some app name')
    ->withAppUrl('https://my-unleash-server.com/api/')
    ->withInstanceId('Some instance id')
    ->withContextProvider(new MyContextProvider())
    ->withStrategy(new AccountIdUnleashStrategy())
    ->build();

The strategy is then simply created in Unleash where you add an accountIds field of type list and mark it as required. Note that this strategy could also be defined using a Gradual rollout strategy with constraints, but I think having a custom one like that provides a better developer experience.

One downside to custom strategies is that if you use them in different projects, you need to create them in each project and the behavior must be the same (meaning the same context fields and the same implementation even across languages).

Unleash in Symfony

The Unleash Symfony bundle handles most of the configuration for you and offers additional features, such as:

  • #[IsEnabled] attribute for controller routes
  • Automatic user ID if the Symfony Security component is configured
  • Automatic integration with the Symfony http request object, like fetching the remote IP from it instead of from the $_SERVER array
  • Automatic environment context value based on the kernel environment
  • Custom context properties configured either as static values, as Expression Language expressions or provided via an event listener
  • Twig functions, tags, tests and filters
  • Automatically registered custom strategies, you simply implement them and Unleash knows about them
  • and more

Additional notes

There are many other Unleash features I haven’t covered, such as the frontend proxy (which handles evaluation and prevents client-side state leakage). Some advanced features are better suited for official documentation rather than a blog post.

 

The problem

If you use a SQLite database in a Doctrine project and enable foreign key checks, you’ll run into an issue with table-modifying migrations: You often need to drop and fully recreate the table. If that table is referenced by others, the migration will fail unless you disable the foreign key checks. Furthermore, the entire migration runs inside a transaction, and SQLite doesn’t allow changing foreign key checks during a transaction.

The solution

There are several possible solutions, but here’s a particularly neat one made possible by PHP 8.4’s new property hooks:

final class VersionXXXXXXXXXXXXXX extends AbstractMigration
{
    protected $connection {
        get {
            $this->connection->executeStatement('PRAGMA foreign_keys = OFF');
            return $this->connection;
        }
        set => $this->connection = $value;
    }

    public function up(Schema $schema): void
    {
        // TODO create migration
    }

    public function down(Schema $schema): void
    {
        // TODO create migration
    }
}

The code above overrides the $connection property from the parent class with a property hook, so every time the migration system requests a connection, the foreign key checks are disabled.

view more: ‹ prev next ›