1. NPM has a global namespace that was shoe-horned into an organization thing later. Packagist has been namespaced since day one. Using a namespace avoids almost all of typo-squatting issues etc.
2. NPM hosts the code at its end. This means you could review some code on GitHub and it might not match the code that you get. Packagist fetches the code from GitHub (same guarantees as Go)
3. NPM: The package version is defined _inside_ the package.json file. This causes issues, because the NPM registry is not necessarily the source of truth for this data, resulting in weird edge cases, such as this bug[0]. Composer/Packagist on the other hand just syncs tags against GitHub.
4. The whole NPM ecosystem needs a whole lot of additional tooling to actually publish packages on NPM. NPM doesn't support robot accounts (PATs are not bots), so you must provide a token with complete write access to your account to your CI system, which then must build and push a package to NPM. This process being without 2FA has led to a lot of compromises. Every release also needs chores to bump the version in package.json (which doesn't have to match the version in package-lock.json). Packagist side-steps all these issues by setting a webhook on your repo (no write access needed) that can trigger a sync on Packagist end. Ideally, this should just work with an RSS feed, but webhooks are easier to build.
NPM has a lot of good ideas (ability to load multiple versions of the same package for eg), but Packagist isn't a terrible package manager - it works quite well, and you rarely see people tripping over composer like how happens with the Python ecosystem. The PHP community also prefers medium sized packages, so you don't get a thousand dependencies accidentally.
[0]: https://github.blog/2021-11-15-githubs-commitment-to-npm-eco...