PHP Packages Graph
Mapping dependencies and maintenance signals across Packagist

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:
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, andsuggest; -
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:
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:
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:
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:
This produces package names in the standard Composer format:
vendor/packageThat format becomes the base identifier for the rest of the system.
The collector then uses each package name to fetch detailed metadata:
/packages/{vendor}/{package}.jsonThe collector saves each package under a path that mirrors its Composer identity:
packages/vendor/package.jsonThe 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:
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:
--fetch-list
--fetch-info
--force-updateThese 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:
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:
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:
7474 -> Neo4j Browser
7687 -> Bolt protocolThis gives the project two interfaces:
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:
Vendor
PackageAnd one ownership relationship:
(v:Vendor)-[:OWNS]->(p:Package)The package node stores the canonical Composer package name:
full_name: vendor/packageThis property becomes the main lookup key for later enrichment and dependency mapping.
The importer also creates uniqueness constraints:
Vendor.name
Package.full_nameGraph 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:
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:
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_typeThis 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.
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:
Package
Downloads
Maintainer
Author
VersionThe Version model captures Composer fields such as:
require
require_dev
suggest
conflict
provide
replace
license
authors
version
version_normalized
abandonedThe Package model then aggregates these fields across all versions.
Aggregated package signals
The model computes package-level values such as:
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:
REQUIRES
DEV_REQUIRES
CONFLICTS
PROVIDES
REPLACES
SUGGESTSRuntime 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:
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:
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.
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:
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:
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:
has_stable_release
published_at
updated_at
abandoned
versionsThese fields help answer questions like:
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:
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:
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:
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:
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:
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:
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:
REQUIRES
DEV_REQUIRES
CONFLICTS
PROVIDES
REPLACES
SUGGESTSKeeping 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:
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.