Download the PHP package wiserwebsolutions/laravel-pde-client without Composer

On this page you can find all versions of the php package wiserwebsolutions/laravel-pde-client. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package laravel-pde-client

wiserwebsolutions/pde-client

Fluent Laravel client for discovering, downloading, and querying data files published by the Pennsylvania Department of Education (PDE). Twelve datasets so far, organized into five categories:

Each of the twelve underlying datasets otherwise publishes one xlsx per school year, except enrollment projections, economically disadvantaged counts, and the Act 1 Index, which are each a single workbook PDE updates in place.

The scraping/downloading/caching core (RemoteFile, DataSource, the FileFinder/FileDownloader contracts, AbstractHtmlFinder, FilesystemDownloader, LocalWorkbookStore, RowTable) is domain-agnostic, so further PDE data can become sibling modules later without touching any of it. See "Extending" below.

Installation

Install via Composer:

Laravel's package auto-discovery registers the service provider and the PDE facade automatically. To customize page URLs, the download disk, the default district, or cache TTLs, publish the config:

Usage

The district/year context

PDE::district() and PDE::query() both return a PendingQuery — shared district/year context that isn't tied to a dataset yet. ->financials(), ->enrollments(), ->assessments(), ->personnel(), and ->community() branch off it into the category's primary fluent query. Every other dataset in a merged category is reached via a sub-method on that primary query:

district() called with no argument (or never called at all) falls back to config('pde-client.default_district') (env PDE_CLIENT_DEFAULT_AUN). year() called with no argument (or never called at all) resolves to the single most recent year available for whatever's being queried. Call ->allYears() (aliases: ->years(), ->year('all')) instead for every year available — the old default. An explicit ->year('2024-2025') (or '2024-25' or 2024) always pins to that one year. A sibling reached via a sub-method (e.g. ->financials()->fundBalance()) carries over whatever district()/year() selection was already made on the primary query.

Querying financial data

Pick budget/actual and a category, get back one FinancialYearSummary per fiscal year (or a single one directly, for a query that resolves to exactly one year - see below), each nesting that year's account-code FinancialRecords in accounts. The workbooks a query needs are downloaded (once) and parsed (cached) automatically.

Notes on the data model:

Rollups and the account hierarchy

Every FinancialRecord carries parentCode and can walk the Chart of Accounts hierarchy directly:

budget/actual on a code with children are always the sum of that code's children (recursively), not whatever the source itself reported at that level - this matters because GFB (budget) never publishes a rollup's amount at all, only leaf-level codes, so without this a query for e.g. 6000 (Total Local Revenue) would come back empty for budget every time. AFR (actual) does publish rollup totals directly, but they're recomputed the same way here too, so budget and actual always reconcile against identical math instead of two independently-sourced totals.

The hierarchy itself is bundled with the package (resources/chart-of-accounts/*.json, trimmed from PDE's own Chart of Accounts manual) - no database or extra install step needed. A handful of codes that show up in real AFR/GFB data (mostly older or program-specific sub-codes, e.g. ARRA-era federal stimulus sub-programs under 8700-8799) aren't in PDE's published manual at all; those come through as parent-less records (parentCode null, parent()/children() simply unavailable) rather than being dropped.

Querying enrollment data

get() folds every dataset the query selected (general enrollment, English learners, projections, and - with ->withEconomicallyDisadvantaged() - economically disadvantaged) into one EnrollmentYearSummary per school year, nesting every matching per-grade EnrollmentRecord underneath in grades (normalized to PK, K, 1-12 — see "Grade normalization" below) rather than discarding the detail. A query that resolves to exactly one year (an explicit ->year(...), or the most-recent-year default) returns that single EnrollmentYearSummary directly, not wrapped in a Collection; a multi-year query (->allYears(), or anything else matching more than one year) returns a Collection of them instead - first()/sole() always give back a single EnrollmentYearSummary regardless (sole() throwing if more than one year matched):

Notes on the data model:

Querying economically disadvantaged enrollment

One EconomicallyDisadvantagedRecord per district per year - economically disadvantaged (low-income) student count, alongside the same-year total enrollment PDE used as the percentage's denominator. A sibling of Enrollment in the enrollments category, reached via ->enrollments()->economicallyDisadvantaged().

Sourced from PDE's single, in-place-updated "Ten Year Low Income and Enrollment History" workbook rather than a per-year file - enrollment here may differ slightly from the general enrollment dataset's own total, since the two come from different PDE reports.

Grade normalization

PDE doesn't publish grades consistently across datasets: general enrollment and English learner counts split pre-K into AM/PM/full-day (PKA/PKP/PKF) and kindergarten into 4- and 5-year-old AM/PM/full-day variants (K4A/K4P/K4F/K5A/K5P/K5F), while projections has just a bare K and no pre-K at all. Grade::normalize() collapses all of that to a single PK, K, 1-12 scale so datasets are comparable; each record's subCounts keeps the raw columns that were summed into it, for callers that want the AM/PM/full-day detail PDE actually reports.

Querying assessment results

PSSA (grades 3-8) and Keystone (grade 11) district proficiency results as a Collection of AssessmentRecords, one per (exam, subject, tested grade, student group), with the percentage of scored students in each proficiency band (0-100, as PDE publishes; null where PDE suppressed populations under 11 students).

Years follow the package's school-year convention: PDE labels these files by the calendar year of the spring testing window, so their "2025" file is year('2024-2025') here. No 2019-2020 data exists (COVID cancelled that administration). Groups are PDE's published cohorts ('All Students', 'Male', 'Female', race/ethnicity groups, 'ELL', 'IEP', 'Economically Disadvantaged').

Querying graduation data

Cohort graduation rates as a Collection of GraduationRecords, one per student group per year, with the 'Total' group also carrying graduate and cohort counts. Rates are fractions (0-1) as PDE stores them. A sibling of Assessments in the assessments category, reached via ->assessments()->graduation().

4-year rates exist 2010-11 onward, 5-year 2011-12, 6-year 2012-13; dropout summaries 2007-08 onward (.xls through 2011-12, .xlsx after).

Querying personnel data

Full-time professional staff summaries as a Collection of PersonnelRecords, one per staff category per year: headcounts by gender plus average salary, years of service, LEA tenure, and education level.

Categories: professional (PDE's "PP" total of the other four don't sum all five), administrator, classroom_teacher, coordinator, other.

Querying Average Daily Membership (ADM)

One AdmRecord per district per year: ADM, WADM, Adjusted ADM, and (2024-25 onward) Nonresident ADM, total ADM for PDE-363, and Special Education ADM. A sibling of Enrollment in the enrollments category, reached via ->enrollments()->averageDailyMembership().

breakdown carries the per-category ADM/WADM detail exactly as PDE publishes it ('ADM Kindergarten HT5' => 1.027, 'WADM Elementary' => 1979.625, ...) - these categories (Pre-K/Kindergarten AM-PM-full-day splits, Elementary, Secondary) are ADM-specific and don't line up with Enrollment's PK/K/1-12 grade scale, so they're kept raw rather than normalized against it.

Querying real estate (millage) tax rates

One RealEstateTaxRateRecord per district per county line - a district spanning more than one county publishes one rate per county, and a handful of counties further split the rate by assessment type. A sibling of Financial in the financials category, reached via ->financials()->realEstateTaxRates().

PDE's own "Municipality / Other Info" column is genuinely mixed-purpose - real municipality/township names, an assessment-type split ("Buildings"/"Land"), an "Oil/Gas/Mineral Properties" carve-out, or a fiscal-year note, depending on the row - so it's kept verbatim as a nullable notes field rather than forced into a municipality-only column. communityCollegeMills is null wherever a district has no additional community college levy.

Querying general fund balance

One FundBalanceRecord per district per year - the year-end general fund balance, broken into committed/assigned/unassigned (account codes 0830/0840/0850) as reported in the AFR. Not to be confused with FinancialQuery::fundBalances(), which covers the GFB's entirely different beginning-of-year budgeted 08xx codes. A sibling of Financial in the financials category, reached via ->financials()->fundBalance().

Querying indebtedness (Statement of Indebtedness)

IndebtednessRecords broken down by fund type and phase - up to 10 per district per year: 2 "all fund types" summary lines (fundType: 'all', phase: 'beginning'|'end') plus 4 phases each ('beginning', 'additional', 'retirements', 'end') for 'governmental' and 'proprietary' fund types. A sibling of Financial in the financials category, reached via ->financials()->indebtedness().

categories breaks total down by PDE's own debt category labels for that year, kept verbatim - the specific categories changed across years (2015-16: Other Long-Term Debt / OPEB / Compensated Absences / Net Pension Liability as four separate lines; 2024-25 onward: consolidated into fewer, differently- named categories, plus new Leases and Extended Term Financing Agreements lines) - a real reporting methodology change, not cosmetic drift, so nothing is forced into a single cross-year taxonomy. Every total (including both "all fund types" lines) is computed from the underlying category values rather than read from the source workbook - PDE's own TOTAL cells are unevaluated spreadsheet formulas with no cached result to read.

Querying Selected Data (including per-pupil expenditure)

One SelectedDataRecord per district per year - a bundle of headline metrics PDE publishes together: aid ratio, WADM/ADM, equalized mills, population density, and PDE's own two raw per-pupil expenditure figures. A sibling of Financial in the financials category, reached via ->financials()->selectedData().

Every metric except wadm is paired with its own *Rank field (statewide rank, 1 = highest) - PDE's own Rank cells are also unevaluated spreadsheet formulas (=RANK(D2,D$2:D$502)), but this workbook does carry a cached result for them, so they read correctly without needing any special handling in this package - see "A note on spreadsheet formulas" below. aidRatio is frequently labeled for a different (often later) school year than the rest of the row, matching PDE's own presentation.

Querying the Act 1 Index

One ActOneIndexRecord per district per year - the maximum property tax increase that district may levy without PDE exception or voter approval. A sibling of Financial in the financials category, reached via ->financials()->actOneIndex().

index is already the adjusted index PDE publishes per district - a fraction (e.g. 0.041 for 4.1%), already multiplied by 0.75 + MV/PI aid ratio for districts PDE adjusts upward (aid ratio over 0.4000). PDE's separate statewide base index (a single percentage per year with no per-district breakdown) isn't modeled here, since it has no district dimension to query by - PDE::actOneIndexFiles()->category('base_index_history') still discovers/downloads that PDF directly if you need it.

Sourced from PDE's single, in-place-updated "Adjusted Index History" workbook rather than a per-year file, the same pattern as economically disadvantaged enrollment.

Discovering and downloading files directly

The lower-level API the query layers are built on:

Every terminal method (get(), first(), sole(), download()) operates on whatever filters were chained before it — category() and matching() are available on every source; schoolYear()/latest() are GFB-specific, revenues()/expenditures()/miscellaneous()/fullReports() are AFR-specific category shortcuts.

download() streams straight from PDE to whichever Laravel filesystem disk you point it at (defaults to config('pde-client.disk')), without buffering the whole file in memory.

Extending to a new PDE data module

The enrollment module (src/Enrollment/) is the template for adding another one (financial data, src/FinancialData/, is the original, enrollment copies its shape almost exactly):

  1. Add a Finders\SomeNewFileFinder extends \WiserWebSolutions\PDEClient\Finders\AbstractHtmlFinder implementing parseDocument(HtmlDocument $document): Collection — the only method that needs to know how that specific page is laid out.
  2. Add a SomeNewFiles extends \WiserWebSolutions\PDEClient\DataSource implementing defaultDirectory() for raw file listing/downloading, and a SomeNewFileLocator (via Support\LocalWorkbookStore) to resolve a RemoteFile to a local path.
  3. Add parser(s) that turn a downloaded workbook into a FinancialData\Parsing\YearTable (reused as-is — it's just districts + amounts[key][code] plain arrays, already cache-safe) and a SomeNewDataRepository that caches parsed tables per year (mirror EnrollmentDataRepository).
  4. Add a SomeNewRecord DTO and SomeNewQuery implements \WiserWebSolutions\PDEClient\Contracts\AcceptsQueryContext, using \WiserWebSolutions\PDEClient\Concerns\HasQueryContext for the district()/year()/allYears() plumbing, with whatever fluent filters make sense for the dataset.
  5. Bind the new Finder in PDEClientServiceProvider::register() (it needs a page URL, same as the existing finders), then wire the dataset in either as a brand new category branch on PendingQuery (e.g. the first real dataset under ->community(), replacing CommunityQuery), or as a sibling sub-method on an existing category's primary query (e.g. adding EnrollmentQuery::someNewDataset() alongside economicallyDisadvantaged() and averageDailyMembership(), seeded via $this->seedSibling(...)).

Nothing about filtering, caching, HTTP fetching, or downloading needs to be touched. That's all in AbstractHtmlFinder and DataSource.

PA.gov's pages are built on the same Adobe Experience Manager template, which means every page repeats the same sidebar <nav>, "the .gov means it's official" <dialog>, and global footer — all of which can contain stray headings/links that a plain //h2 or //a[...] XPath query would pick up alongside the page's real content (this bit both the financial-data Finders during development; see AbstractHtmlFinder::excludingChrome()). Wrap any XPath predicate you write with it, e.g. "//a[".self::excludingChrome("substring(@href, ...) = '.xlsx'")."]".

A note on spreadsheet formulas

Several PDE workbooks contain formula cells (=SUM(...), =RANK(...)) that were evidently never opened in Excel to compute before being published, or were - it varies by file, and sometimes by column within the same file. SpreadsheetReader (.xlsx via openspout) already prefers a formula cell's cached computed value when the workbook has one, so most Parsers never need to think about this at all. When a workbook genuinely has no cached value (confirmed on the Statement of Indebtedness workbook's TOTAL cells - see IndebtednessParser), the reader falls back to the raw formula text, which will silently fail is_numeric()/is_int()/is_float() checks and come through as null - if a new Parser's numeric column looks suspiciously empty across every row, dump a raw cell value first to rule this out before assuming a header-matching bug. The fix is to compute the value yourself from the same cells the formula would have referenced (IndebtednessParser sums its own category columns; an Excel-compatible RANK() would need a full column of values and tie-aware ranking - not currently needed anywhere, since every Rank column encountered so far has had a usable cached value).

Testing

This package ships without tests pre-written for the live Finders, since they were verified against PDE's real pages during development (structure can drift if PDE redesigns the site). If you add tests, Http::fake() the listing page URLs with saved HTML fixtures and assert on Finder::find()'s resulting RemoteFile collection. No network access needed at test time.


All versions of laravel-pde-client with dependencies

PHP Build Version
Package Version
Requires php Version ^8.2
ext-dom Version *
illuminate/support Version ^10.0|^11.0|^12.0|^13.0
illuminate/http Version ^10.0|^11.0|^12.0|^13.0
illuminate/filesystem Version ^10.0|^11.0|^12.0|^13.0
illuminate/contracts Version ^10.0|^11.0|^12.0|^13.0
openspout/openspout Version ^4.24
phpoffice/phpspreadsheet Version ^5.9
spatie/laravel-data Version ^4.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package wiserwebsolutions/laravel-pde-client contains the following files

Loading the files please wait ...