Research2025

PHP Packages Graph

Mapping dependencies and maintenance signals across Packagist

Cover image for PHP Packages Graph

PHP Packages Graph is a research project about the structure of the PHP package ecosystem. It collects Composer metadata from Packagist and stores packages, vendors, and dependency types in Neo4j.

Download counts say little about where a package sits in the wider dependency network. A graph makes it possible to compare direct and development dependencies, inspect abandoned packages with many dependants, and study licenses or maintenance signals across connected projects.

The problem

Packagist makes package metadata available, and Composer resolves dependencies for individual projects. Neither one is meant to explain the structure of the full ecosystem.

Packagist and Composer make it easy to install packages, but they do not directly answer deeper ecosystem questions:

Mermaid

A package manager is excellent at resolving dependencies for one project. It is not designed to explain the dependency structure of an entire ecosystem.

Modern PHP applications often depend on dozens or hundreds of direct and transitive packages. Some libraries sit beneath a large part of the ecosystem without appearing in the product itself.

The ecosystem is therefore a graph problem.

A package can require another package. A vendor can own many packages. A package can conflict with, replace, provide, suggest, or depend on another package only in development. These relationships are not secondary metadata. They are the structure of the ecosystem itself.

That turned PHP Packages Graph from a simple Packagist data collection script into a graph-based research system.

Research direction

The initial research question led to a short brief:

Build a dependency graph of PHP packages published on Packagist.

That idea quickly became more interesting. A useful system had to do more than fetch package JSON files. It needed to:

  • collect package names from Packagist;

  • fetch detailed package metadata;

  • store the raw dataset locally for replay and inspection;

  • normalize package records across versions;

  • extract dependencies from require, require-dev, conflict, provide, replace, and suggest;

  • model vendors, packages, and dependency relationships in Neo4j;

  • preserve useful metadata such as licenses, authors, downloads, repository links, package type, abandoned status, and update dates;

  • expose the graph through Cypher queries;

  • support research questions around influence, stability, licensing, and maintenance.

The project therefore needed a repeatable pipeline from Packagist metadata to a graph that I could query and rebuild.

System architecture

PHP Packages Graph uses four stages:

Mermaid

The collector downloads package records. Pydantic validates and aggregates version metadata. Neo4j stores packages and their typed relationships. Cypher queries and notebooks analyze the graph.

The system follows this flow:

Mermaid

The saved JSON can be inspected or replayed before any Neo4j import runs.

Data collection layer

The collector turns Packagist records into a local research dataset. Each record includes versions, dependencies, downloads, repository information, maintainers, licenses, and package types.

The collector follows one rule:

Data collection should be reproducible, resumable, and separate from graph import.

PHP Packages Graph writes the API responses to local JSON before importing anything into Neo4j.

The collection workflow is simple:

Mermaid

Fetching the registry takes many network requests. Saving the responses lets an interrupted download resume and lets graph imports or notebooks run without live API calls.

Package list collection

The first step fetches the global package list from Packagist:

Plain text
/packages/list.json

This produces package names in the standard Composer format:

Plain text
vendor/package

That format becomes the base identifier for the rest of the system.

The collector then uses each package name to fetch detailed metadata:

Plain text
/packages/{vendor}/{package}.json

The collector saves each package under a path that mirrors its Composer identity:

Plain text
packages/vendor/package.json

The filesystem mirrors Composer names, so vendor/package maps to packages/vendor/package.json.

Incremental fetching

The collector checks whether package information already exists before fetching it again.

The existence check creates this incremental path:

Mermaid

Packagist contains many packages. Refetching every record on each run would waste time and bandwidth.

The current version uses request timeouts, local existence checks, and command-line flags:

Plain text
--fetch-list

--fetch-info

--force-update

These controls are enough to resume the current dataset build.

Why the collection design matters

The collector only records API responses. Models, graph import, and research queries run as later steps:

Mermaid

Each later step can rerun against the saved dataset without downloading every package again.

Graph database layer

Once the package metadata is collected, the next challenge is representation.

A relational table can store package rows. A document store can store package JSON. But dependency ecosystems are relationship-heavy by nature.

Neo4j stores these relations as edges rather than join records that every analysis must reconstruct.

The graph database follows one rule:

Model the PHP ecosystem as a network of relationships, not as a flat list of packages.

At the center of the graph is the Package node.

A package belongs to a vendor and connects to other packages through Composer dependency relationships:

Mermaid

This model makes the ecosystem queryable in the same shape as the real dependency domain.

A package is a record and a node in a dependency network.

Technology choice

PHP Packages Graph uses Neo4j as the graph database.

The project runs Neo4j through Docker Compose and enables APOC and Graph Data Science plugins, which are useful foundations for graph-oriented analysis and future centrality algorithms.

The local Neo4j service exposes:

Plain text
7474 -> Neo4j Browser

7687 -> Bolt protocol

This gives the project two interfaces:

Mermaid

That split works well for a research project. Python handles data processing, while Neo4j handles graph storage and interactive analysis.

Core graph model

The first graph import creates two node types:

Plain text
Vendor

Package

And one ownership relationship:

Cypher
(v:Vendor)-[:OWNS]->(p:Package)

The package node stores the canonical Composer package name:

Plain text
full_name: vendor/package

This property becomes the main lookup key for later enrichment and dependency mapping.

The importer also creates uniqueness constraints:

Plain text
Vendor.name

Package.full_name

Graph imports must be safe to rerun after an interruption or model change.

Without uniqueness constraints, the database could silently create duplicate vendors or duplicate package nodes. With constraints and MERGE, imports become more predictable and idempotent.

Graph schema

The graph schema can be represented as a compact ecosystem model:

Mermaid

This schema keeps the model simple while preserving the relationships needed for ecosystem analysis.

Package as a canonical knowledge object

After the importer creates the nodes, the mapping step adds metadata to each package:

Plain text
description

published_at

updated_at

licenses

versions

authors

repository

github_stars

github_watchers

github_forks

github_open_issues

language

abandoned

downloads

type

has_stable_release

is_custom_type

This turns each Package node into a compact knowledge object.

The graph stores the package type, stable release status, licenses across versions, download count, publication date, last update, and abandoned status.

That combination is what makes the graph useful for research.

Dependency relationships explain structure. Package properties explain context.

Modeling and normalization layer

Packagist package metadata is nested, versioned, and inconsistent across packages.

A package can have many versions. Each version can define its own dependencies, licenses, authors, type, stability, and replacement rules.

The modeling code aggregates version records into package-level properties and relationship lists.

Mermaid

The model follows one rule:

Preserve version-level metadata, but expose package-level signals for graph analysis.

PHP Packages Graph uses Pydantic models to validate and structure package data before importing it into Neo4j.

The model includes package-level objects such as:

Plain text
Package

Downloads

Maintainer

Author

Version

The Version model captures Composer fields such as:

Plain text
require

require_dev

suggest

conflict

provide

replace

license

authors

version

version_normalized

abandoned

The Package model then aggregates these fields across all versions.

Aggregated package signals

The model computes package-level values such as:

Plain text
aggregate_versions()

aggregate_licenses()

aggregate_authors()

aggregate_require()

aggregate_require_dev()

aggregate_suggest()

aggregate_conflict()

aggregate_provide()

aggregate_replace()

has_stable_version()

last_updated_time()

is_custom_type()

Python prepares package-level properties and relationship lists before the import. Cypher queries can then read a package node without traversing every raw version object.

Dependency relationship mapping

The dependency mapping layer creates explicit graph relationships from Composer metadata:

Plain text
REQUIRES

DEV_REQUIRES

CONFLICTS

PROVIDES

REPLACES

SUGGESTS

Runtime and development dependencies remain separate. A package common in require-dev may support tests, static analysis, coding standards, or debugging without running in production.

By keeping these relationship types separate, PHP Packages Graph can answer more precise questions:

Mermaid

A flat package table would have to rebuild those typed relations for every query.

Why the modeling design matters

The modeling layer simplifies Packagist records without discarding the distinctions used in the analysis.

The import keeps only the Composer distinctions used by the research questions.

PHP Packages Graph keeps the important distinctions:

Mermaid

The resulting graph preserves package identity, ownership, dependency types, maintenance, licenses, and release stability.

Analysis layer

After the importer builds the graph, the analysis uses Cypher.

The analysis layer asks ecosystem-level questions directly against the graph.

Mermaid

The analysis layer follows one rule:

Turn package metadata into ecosystem-level questions.

The project includes Cypher queries for license distribution, top required packages, development dependencies, packages that appear in both dependency categories, downloads, author averages, release years, and long-maintained packages.

Those queries define the research direction of the project.

License distribution

Licensing is one of the most important ecosystem-level signals.

The graph can answer:

Mermaid

Licenses affect adoption, compliance, redistribution, and organizational risk.

A dependency graph without license analysis only tells part of the story.

Dependency influence

The most important graph question is package influence.

A package with many incoming REQUIRES relationships is structurally important because many other packages depend on it.

The graph can answer:

Mermaid

This reveals the infrastructure layer of the PHP ecosystem.

Some packages may not look exciting as products, but they are deeply embedded in the software supply chain. Those packages deserve attention because their maintenance, stability, and security affect many downstream projects.

Downloads and dependency centrality

Downloads and dependency centrality are related, but they are not the same signal.

Downloads measure usage volume.

Incoming dependency relationships measure structural influence.

A package can have high downloads because it is installed directly by many projects. Another package can be structurally important because it sits underneath other popular packages.

PHP Packages Graph makes it possible to compare these signals instead of treating popularity as one-dimensional.

That is important because ecosystem health depends on both visible and invisible infrastructure.

Stability and maintenance

The project also studies stability and maintenance through fields such as:

Plain text
has_stable_release

published_at

updated_at

abandoned

versions

These fields help answer questions like:

Mermaid

That turns package metadata into sustainability signals.

A dependency ecosystem is not healthy only because it is large. It is healthy when important packages are maintained, stable, understandable, and legally usable.

Query examples

The project uses Cypher queries to explore the graph.

For example, license distribution can be queried with:

Cypher
MATCH (p:Package)

UNWIND p.licenses AS license

RETURN license, COUNT(p) AS package_count

ORDER BY package_count DESC;

Top runtime dependencies can be queried with:

Cypher
MATCH (:Package)-[:REQUIRES]->(p:Package)

RETURN p.full_name AS package, COUNT(*) AS times_required

ORDER BY times_required DESC

LIMIT 100;

Top development dependencies can be queried with:

Cypher
MATCH (:Package)-[:DEV_REQUIRES]->(p:Package)

RETURN p.full_name AS package, COUNT(*) AS times_required

ORDER BY times_required DESC

LIMIT 100;

Packages that are important in both runtime and development contexts can be queried with:

Cypher
MATCH (p:Package)

OPTIONAL MATCH (:Package)-[:REQUIRES]->(p)

WITH p, COUNT(*) AS required_count

OPTIONAL MATCH (:Package)-[:DEV_REQUIRES]->(p)

WITH p, required_count, COUNT(*) AS dev_required_count

WHERE required_count > 0 AND dev_required_count > 0

RETURN p.full_name, required_count, dev_required_count

ORDER BY required_count DESC, dev_required_count DESC

LIMIT 10;

Old maintained packages can be queried with:

Cypher
MATCH (p:Package)

WHERE (p.abandoned IS NULL OR p.abandoned = false)

  AND p.published_at IS NOT NULL

  AND p.updated_at IS NOT NULL

WITH DISTINCT p,

     duration.between(datetime(p.published_at), datetime(p.updated_at)) AS lifespan

ORDER BY lifespan.years DESC

LIMIT 10

RETURN DISTINCT p.full_name AS package_name, p.published_at, p.updated_at, lifespan.days;

These queries turn the stored graph into a research tool.

Research notebook and figures

The repository also includes a notebook and figures, which makes sense for the project direction.

The codebase includes both the importer and an exploratory research environment.

The notebook inspects results, builds charts, compares metrics, and turns graph queries into visual explanations.

The notebook turns exported query results into charts, tables, and research notes.

The research workflow looks like this:

Mermaid

The figures link each interpretation back to an exported query result.

Key engineering decisions

Start with the ecosystem question, not the tool

The project started from a research question:

What does the PHP ecosystem look like when Packagist packages are modeled as a dependency graph?

The question requires typed dependency relations, so Neo4j stores the imported dataset as a graph.

Use a graph database for dependency relationships

Package ecosystems are networks.

A package can require, suggest, replace, conflict with, or provide another package. The graph keeps each relation explicit.

Neo4j stores each Composer relation type as an edge that Cypher can count or traverse.

Separate collection from import

The collector writes a local JSON dataset before data enters Neo4j.

The local files let the import rerun without Packagist being available.

Keep relationship types explicit

The project does not collapse every edge into a generic DEPENDS_ON relationship.

It keeps Composer relationship types separate:

Plain text
REQUIRES

DEV_REQUIRES

CONFLICTS

PROVIDES

REPLACES

SUGGESTS

Keeping each relationship type avoids mixing runtime and development influence in the first analysis.

Aggregate package metadata before graph insertion

The Pydantic model aggregates licenses, authors, versions, dependencies, stability, and update timestamps before writing to Neo4j.

The graph stores the aggregated values that the queries use instead of the full nested Packagist response.

Use Cypher as the research interface

Cypher expresses questions such as "top required packages" and "packages required at runtime and in development" by matching typed edges.

Prepare for graph algorithms

The Neo4j setup includes the Graph Data Science plugin, which creates room for deeper analysis later.

The natural next step is to compute:

Mermaid

That would push the project from descriptive analysis toward structural ecosystem research.

Treat maintenance as an ecosystem signal

The project examines downloads and dependency counts alongside maintenance fields.

It also includes fields such as abandoned status, publication date, update date, stable release presence, versions, and package type.

Popularity alone does not describe the health of a dependency.

A heavily depended-on package that is abandoned is a risk. An old package that is still maintained is infrastructure. A package with no stable release may be useful, but it carries a different kind of adoption signal.

What I learned

The biggest lesson from PHP Packages Graph is that direct installation counts miss much of the ecosystem. A package buried deep in dependency trees may support many applications without appearing in their top-level configuration. The graph makes that position queryable.

Package metadata also becomes more useful when it is connected. A license, download count, or abandoned flag gains context when it can be compared with incoming dependencies.

For example, an abandoned package with few dependents is one kind of problem. An abandoned package with thousands of incoming dependency relationships is a very different problem.

The project also showed that ecosystem research depends on data preparation. Packagist nests uneven metadata across versions. Pydantic validates those records, and the importer normalizes and aggregates them before analysis.

Neo4j changes the questions I can ask once packages are nodes and each Composer dependency type is an explicit edge.

Current research

The project now collects Packagist records incrementally, keeps a local copy of the source data, normalizes metadata across versions, and imports explicit dependency relationships into Neo4j. Cypher queries and notebooks cover licenses, dependency counts, downloads, release stability, and maintenance dates.

The next useful work is to publish a dated dataset and run graph measures such as PageRank, betweenness, and community detection. That would let the analysis move from descriptive counts to questions about structural importance and maintenance risk.

More projects

View all

2026

Leganews Pro

A search and monitoring platform for Congolese legal texts

View

2025

Basango

A low-cost pipeline for collecting and organizing news from Congolese media

View