Skip to content

Instantly share code, notes, and snippets.

@loilo
Last active June 17, 2026 08:34
Show Gist options
  • Select an option

  • Save loilo/36cb8564e912ec67c11b5fc9eadc3e2a to your computer and use it in GitHub Desktop.

Select an option

Save loilo/36cb8564e912ec67c11b5fc9eadc3e2a to your computer and use it in GitHub Desktop.
Assemble CSS Media Queries in PHP

PHP Media Query Builder

Fluent, immutable interface for creating CSS Media Queries in PHP.

use function Loilo\MediaQuery\mq;

$query = mq('a: true')
	->or(mq('b: true')->and('c: true'))
	->and('d: true');

echo $query;
// (a: true) and (d: true), (b: true) and (c: true) and (d: true)

Methods

negate()

Negates the current media query.

Example:

mq('width >= 100px')->or('height >= 100px')->negate();

and(string|MediaQuery $query)

Requires to match both the current and the given media query.

Example:

mq('width >= 100px')->and('width < 300px');

or(string|MediaQuery $query)

Requires to match at least one of the current and the given media query.

Example:

mq('width >= 100px')->or('height >= 100px');

and_not(string|MediaQuery $query)

Requires to match the current media query but not the given media query.

Example:

mq('width >= 100px')->and_not('width >= 300px');

or_not(string|MediaQuery $query)

Requires to match at least one of the current media query and the negation of the given media query.

Example:

mq('width >= 100px')->or_not('height < 100px');
<?php
namespace Loilo\MediaQuery;
/**
* Base class for media query combinations
*/
abstract class MediaQuery
{
/**
* Combine with another query using AND logic
*/
public function and(string|MediaQuery $other): MediaQuery
{
if (is_string($other)) {
$other = new Atom($other);
}
return new Conjunction($this, $other);
}
/**
* Combine with another query using AND NOT logic
*/
public function and_not(string|MediaQuery $other): MediaQuery
{
if (is_string($other)) {
$other = new Atom($other);
}
return new Conjunction($this, new Negation($other));
}
/**
* Combine with another query using OR logic
*/
public function or(string|MediaQuery $other): MediaQuery
{
if (is_string($other)) {
$other = new Atom($other);
}
return new Disjunction($this, $other);
}
/**
* Combine with another query using OR NOT logic
*/
public function or_not(string|MediaQuery $other): MediaQuery
{
if (is_string($other)) {
$other = new Atom($other);
}
return new Disjunction($this, new Negation($other));
}
/**
* Negate this query
*/
public function negate(): MediaQuery
{
return new Negation($this);
}
/**
* Convert to Negation Normal Form (negations only applied to atoms)
*/
abstract protected function to_nnf(): MediaQuery;
/**
* Convert to Disjunctive Normal Form
*/
abstract protected function to_dnf(): MediaQuery;
/**
* Serialize to string (normalizes to DNF first)
*/
public function __toString()
{
return $this->to_dnf()->serialize();
}
/**
* Internal serialization method (called after normalization)
*/
protected function serialize(): string
{
return '';
}
}
/**
* Represents a single atomic media query (e.g., "min-width: 400px")
*/
class Atom extends MediaQuery
{
private readonly string $query;
public function __construct(string $query)
{
$this->query = $query;
}
protected function to_nnf(): MediaQuery
{
return $this;
}
protected function to_dnf(): MediaQuery
{
return $this;
}
protected function serialize(): string
{
return '(' . $this->query . ')';
}
}
/**
* Represents a negated media query (e.g., "not (min-width: 400px)")
*/
class Negation extends MediaQuery
{
private readonly MediaQuery $query;
public function __construct(MediaQuery $query)
{
$this->query = $query;
}
protected function to_nnf(): MediaQuery
{
if ($this->query instanceof Atom) {
return $this;
}
if ($this->query instanceof Negation) {
$result = $this->query->query->to_nnf();
return $result;
}
if ($this->query instanceof Conjunction) {
$left = new Negation($this->query->left);
$left = $left->to_nnf();
$right = new Negation($this->query->right);
$right = $right->to_nnf();
$disj = new Disjunction($left, $right);
return $disj->to_nnf();
}
if ($this->query instanceof Disjunction) {
$left = new Negation($this->query->left);
$left = $left->to_nnf();
$right = new Negation($this->query->right);
$right = $right->to_nnf();
$conj = new Conjunction($left, $right);
return $conj->to_nnf();
}
return $this;
}
protected function to_dnf(): MediaQuery
{
$nnf = $this->to_nnf();
if ($nnf instanceof Negation) {
return $nnf;
}
return $nnf->to_dnf();
}
protected function serialize(): string
{
return "(not {$this->query})";
}
}
/**
* Represents a conjunction of media queries (e.g., "A and B and C")
*/
class Conjunction extends MediaQuery
{
public readonly MediaQuery $left;
public readonly MediaQuery $right;
public function __construct(MediaQuery $left, MediaQuery $right)
{
$this->left = $left;
$this->right = $right;
}
protected function to_nnf(): MediaQuery
{
$left = $this->left->to_nnf();
$right = $this->right->to_nnf();
return new Conjunction($left, $right);
}
protected function to_dnf(): MediaQuery
{
$left = $this->left->to_dnf();
$right = $this->right->to_dnf();
if ($left instanceof Disjunction) {
$c1 = new Conjunction($left->left, $right);
$c2 = new Conjunction($left->right, $right);
$disj = new Disjunction($c1, $c2);
return $disj->to_dnf();
}
if ($right instanceof Disjunction) {
$c1 = new Conjunction($left, $right->left);
$c2 = new Conjunction($left, $right->right);
$disj = new Disjunction($c1, $c2);
return $disj->to_dnf();
}
return new Conjunction($left, $right);
}
protected function serialize(): string
{
return $this->left . ' and ' . $this->right;
}
}
/**
* Represents a disjunction of media queries (e.g., "A, B, C" - comma-separated OR)
*/
class Disjunction extends MediaQuery
{
public readonly MediaQuery $left;
public readonly MediaQuery $right;
public function __construct(MediaQuery $left, MediaQuery $right)
{
$this->left = $left;
$this->right = $right;
}
protected function to_nnf(): MediaQuery
{
$left = $this->left->to_nnf();
$right = $this->right->to_nnf();
return new Disjunction($left, $right);
}
protected function to_dnf(): MediaQuery
{
$left = $this->left->to_dnf();
$right = $this->right->to_dnf();
return new Disjunction($left, $right);
}
protected function serialize(): string
{
return $this->left . ', ' . $this->right;
}
}
/**
* Factory function to create a media query atom
*/
function mq(string $query): MediaQuery
{
return new Atom($query);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment