Skip to content

markhj/php-tendency

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

7 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

PHP Tendency banner

GitHub Tag Codacy Badge License: MIT

PHP Tendency is a random value generator. But it's not the one you'd pick to find lottery numbers, rather it's the one you'd choose to calculate complex scenarios, where tens or hundreds of factors impact the likelihood of an outcome.

πŸ’« Sounds cool, tell me more

PHP Tendency had its beginning as a "spin-off" from a political simulation game, in which the choices of people, countries and companies needed a degree of randomness, but never total randomness.

The risk that a person commits a crime isn't 50/50. It's determined by personality, criminal history, how the person was raised, life circumstances, social circles, and so forth.

Implementing such complex determination in code isn't easy, and without the proper tooling, it's outright cumbersome: Hard to test, hard to predict, and hard to keep in check.

This is the exact problem PHP Tendency set out to solve. Let's imagine a function which calculates the risk of someone committing a crime. For a prototype, most would probably do something like:

class Person
{
    public function shouldCommitCrime(): bool
    {
        $risk = 0;
        if ($this->personality->integrity < 0.3) {
            $risk += 0.1;
        }
        if ($this->criminalHistory->count() > 0) {
            $risk += 0.15;
        }
        // ... And hundreds more conditions
        return $risk > 0.5;
    }
}

But... What if that same thing could look like this?

public function shouldCommitCrime(): bool
{
    return $this->randomizer()
        ->hasLow(Personality::Integrity)
        ->hasCriminalRecord()
        // And so forth
        ->compute()
        ->result;
}

Much better, right? This is easier to test, easier to maintain, and easier to read.

"But where do all these methods like hasLow and hasCriminalRecord come from?" What a great question! They come from extensions. Think of them as custom modules you write specifically for your project. We'll get back to that later in this guide. But they basically provide reusability, modularity, improved overview, and testability.

πŸƒ Features

  • Extensible and modular: Create separate classes which are injected into a randomizer per an on-demand basis.
  • Biased outcomes: Make certain outcomes more likely than others using a weight-based randomization (mean + standard deviation).
  • Several types: Int, float or bool? Doesn't matter. You can also build your own custom randomizer which takes strings, arrays, or specific classes. The sky is the limit.

πŸ“Œ Prerequisites

  • PHP 8.3 or higher

πŸ“¦ Installation

The library is easily installed with Composer:

composer require markhj/php-tendency

πŸš€ Getting started

Let's have a look at the fundamental usage of PHP Tendency. Take note that the really fun stuff comes in the Extensions chapter.

There are a set of base classes called "randomizers". Three are bundled out-of-the-box with PHP Tendency, but you can easily build more on your own, simply by extending RandomBase.

Boolean

To retrieve a random boolean value:

use Markhj\PhpTendency\RandomBool;
(new RandomBool())->compute();

Integer

Retrieve a random integer between a min and max.

use Markhj\PhpTendency\RandomInt;
(new RandomInt(15, 35))->compute();

Float

Retrieve a random floating-point value between a min and max.

use Markhj\PhpTendency\RandomInt;
(new RandomInt(-25.0, 25.0))->compute();

Bias

All of these classes sport a changeMean method, which moves the bias. The bias expresses the most likely outcome, which by default is 0.5 (50%, the middle).

And yes, this also means that if you want a random value between 0 and 100, getting a number around 50 is more likely than numbers close the bounds. Again: This isn't the choice for a lottery number, where you'd want an un-biased pick.

However, the trick is that you can move the bias, so if you move it to 0.3 (30%), then a number around 30 is most likely.

You can sway the mean using these functions:

(new RandomBool())->changeMean(-0.25);  // More likely to be false
(new RandomBool())->changeMean(0.25);   // More like to be true
(new RandomInt(0, 100))->changeMean(0.25);  // More like to land around 75

Keep in mind that changeMean isn't a setter, it increments or decreases.

The mean value starts at 0.5, and would be between 0 and 1 for most use-cases. But it's perfectly fine to fall outside of this bound.

The RandomizedResult class

Randomizers return an instance of RandomizedResult. This object provides some information on top of the computed random value. These values are mainly useful for testing, but should you have some reason to use them... Well, they are there.

Property Type Description
mean float The final mean value used after extensions have manipulated it.
computed float The final computed random value (between 0.0 and 1.0).
result mixed The actual result, typically a number between X and Y, boolean, or something else.

Standard deviation

You can modify the standard deviation on all of the above classes. The standard deviation is an expression of "how far" a random value typically falls from the mean.

Example:

(new RandomFloat(10.0, 25.0, 0.25))->compute();

Here, the standard deviation is 0.25 which corresponds to 25% from the mean value.

Learn more: Standard deviation on Wikipedia

πŸ’‘ Extensions

Okay, so what we've seen so far is pretty dull. But it's necessary to know it, to get to the fun part -- which we have finally reached.

If randomizers represent the heart of PHP Tendency, then extensions represent the brain, liver, kidney and spleen. PHP Tendency makes only limited sense without them.

The idea

The ultimate goal of an extension is to manipulate the mean value contained in the randomizer (.e.g RandomBool or RandomFloat).

The mean value always starts at 0.5 (perfectly between 0.0 and 1.0), which means the random value gravitates towards the middle of whatever you're looking to randomize.

It's perfectly fine if the mean moves below zero or above one.

If we go back to trying to determine if a person should commit a crime or not, then having a criminal history should sway the mean towards 1, increasing the likelihood of that outcome.

The ultimate purpose of extensions is to sway the mean in negative or positive direction, creating bias towards a specific outcome.

Creating an extension

Creating a basic extension is as easy as:

class SimpleExtension implements Extension
{
    #[Expose]
    public function myFunc(Extendable $random, float $change): Extendable
    {
        return $random->changeMean($change);
    }
}

There are a few things to note down:

  • The extension class must implement the Extension interface.
  • Every method that must be accessible through the randomizer, must have the #[Expose] attribute.
  • The first argument of an exposed method must be the randomizer (Extendable). The randomizer instance which will be injected, when the method is called.
  • Exposed methods must return Extendable (i.e. the randomizer).

You can explore ExtensionTest for a real-life example.

Usage

When you want to use your extension, it must first get injected into the randomizer. Once that's done, you have access to its exposed methods.

Here, we extend a RandomFloat with the amazing extension we just built.

$randomizer = new RandomFloat(50.0, 75.0);
$randomizer->extend(new SimpleExtension());

Now, you have access to the myFunc method:

$randomizer->myFunc(0.3);

What happens in this particular example, is that the randomizer's mean value (which starts at 0.5) is increased by 0.3, to 0.8.

That in effect means that RandomFloat with min/max at 50 to 75, is more likely to produce a number around 70, because we have shifted the bias.

Notice a lot of "maybe" and "probably". This is because we use biased randomization with standard distribution. No outcome is completely guaranteed, which is exactly what we want: A tendency.

A person with a criminal history may have a tendency to commit crime, but they don't always do it.

Tip! Extensions can themselves take parameters. In our crime determination example, we would for instance provide information about the person as constructor arguments.

What about the criminal?

Don't worry, we haven't forgotten our hypothetical criminal.

Let's imagine we have an extension class like this:

class PersonTendency implements Extension
{
    public function __construct(
        private Person $person,
    ) {
    }

    #[Expose]
    public function hasCriminalRecord(Extendable $randomizer): Extendable
    {
        $records = getCriminalRecordFromDatabase($this->person);
        
        // Increase the likelihood 10% (+0.1) per record 
        $randomizer->changeMean(count($records) / 10);
        
        // You could even look at the severity of the records, and other stuff
        
        return $randomizer;
    }
}

And where you use this in your actual logic, it would now look like:

$shouldCommitCrime = (new RandomBool())
    ->extend(new PersonTendency($somePerson))
    ->hasCriminalRecord()
    ->compute();

The idea is now that you add tens or hundreds of factors, where each factor sways the mean in a direction.

Keep in mind: It's perfectly acceptable that the mean falls below 0.0 (0%) or above 1.0 (100%).

πŸ’ Working on the project

This information is good to know if you want to fork the repository, or even contribute to the original.

Testing

You can run the test suite using:

composer test

Linting

Linting is carried out with Laravel Pint. Point your IDE to use the file pint.json.

About

A tool to make complex randomization scenarios readable and modular.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages