diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d4379c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/vendor/ +/.idea/ +composer.phar +build/ +resources/ \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..90272fe --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,130 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..53d1f3d --- /dev/null +++ b/LICENSE @@ -0,0 +1,675 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..e08bd39 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Cadfael + +Cadfael is static analysis tool to provide critiquing for databases. + +At the moment Cadfael focuses on the MySQL database but the code is structured in a way to allow other databases to be added easily. + +## Installation + +```bash +composer require cadfael/cadfael +``` + +## Usage + +```bash +./vendor/bin/cadfael run --host 127.0.0.1 --username root --port 3306 [database_to_scan] +``` + +**Output** +``` +Cadfael CLI Tool + +Host: localhost:3306 +User: [username] + +Attempting to scan schema tests +What is the database password? + ++----------------------+------------------------------------------+----------+----------------------------------------------------------------------------------+ +| Check | Entity | Status | Message | ++----------------------+------------------------------------------+----------+----------------------------------------------------------------------------------+ +| SaneInnoDbPrimaryKey | table_with_insane_primary_key | Warning | In InnoDB tables, the PRIMARY KEY is appended to other indexes. | +| | | | If the PRIMARY KEY is big, other indexes will use more space. | +| | | | Maybe turn your PRIMARY KEY into UNIQUE and add an auto_increment PRIMARY KEY. | +| | | | Reference: https://dev.mysql.com/doc/refman/5.7/en/innodb-index-types.html | +| EmptyTable | table_with_insane_primary_key | Warning | Table contains no records. | +| RedundantIndexes | table_with_insane_primary_key | Concern | Redundant index full_name (superseded by full_name_height_in_cm). | +| | | | A redundant index can probably drop it (unless it's a UNIQUE, in which case the | +| | | | dominant index might be a better candidate for reworking). | +| | | | Reference: https://dev.mysql.com/doc/refman/5.7/en/sys-schema-redundant-indexes. | +| | | | html | +| ReservedKeywords | table_with_insane_primary_key.name | Concern | `name` is a reserved keyword in MySQL 8.0. | +| | | | Avoid using reserved words as a column name. | +| | | | Reference: https://dev.mysql.com/doc/refman/8.0/en/keywords.html | +| ReservedKeywords | table_with_keyword_columns.some | Concern | `some` is a reserved keyword in MySQL 8.0. | +| | | | Avoid using reserved words as a column name. | +| | | | Reference: https://dev.mysql.com/doc/refman/8.0/en/keywords.html | +| ReservedKeywords | table_with_keyword_columns.avg | Concern | `avg` is a reserved keyword in MySQL 8.0. | +| | | | Avoid using reserved words as a column name. | +| | | | Reference: https://dev.mysql.com/doc/refman/8.0/en/keywords.html | +| SaneAutoIncrement | table_with_non_primary_autoincrement.aut | Warning | This field should be set as the primary key. | +| | o_incremental | | | +| EmptyTable | table_with_signed_autoincrement | Warning | Table contains no records. | +| SaneAutoIncrement | table_with_signed_autoincrement.id | Warning | This field should be an unsigned integer type. | +| RedundantIndexes | table_with_unused_index | Concern | Redundant index some (superseded by some_avg). | +| | | | A redundant index can probably drop it (unless it's a UNIQUE, in which case the | +| | | | dominant index might be a better candidate for reworking). | +| | | | Reference: https://dev.mysql.com/doc/refman/5.7/en/sys-schema-redundant-indexes. | +| | | | html | +| ReservedKeywords | table_with_unused_index.some | Concern | `some` is a reserved keyword in MySQL 8.0. | +| | | | Avoid using reserved words as a column name. | +| | | | Reference: https://dev.mysql.com/doc/refman/8.0/en/keywords.html | +| ReservedKeywords | table_with_unused_index.avg | Concern | `avg` is a reserved keyword in MySQL 8.0. | +| | | | Avoid using reserved words as a column name. | +| | | | Reference: https://dev.mysql.com/doc/refman/8.0/en/keywords.html | +| EmptyTable | table_with_utf8_encoding | Warning | Table contains no records. | +| CorrectUtf8Encoding | table_with_utf8_encoding.utf8_encoding | Concern | Character set should be utf8mb2 not utf8. | +| | | | Reference: https://www.eversql.com/mysql-utf8-vs-utf8mb4-whats-the-difference-be | +| | | | tween-utf8-and-utf8mb4/ | +| MustHavePrimaryKey | table_without_primary_key | Critical | Table must have a PRIMARY KEY | +| | | | Reference: https://federico-razzoli.com/why-mysql-tables-need-a-primary-key. | +| | | | MySQL 8 replication will break if you have InnoDB tables without a PRIMARY KEY. | +| ReservedKeywords | table_without_primary_key.name | Concern | `name` is a reserved keyword in MySQL 8.0. | +| | | | Avoid using reserved words as a column name. | +| | | | Reference: https://dev.mysql.com/doc/refman/8.0/en/keywords.html | +| EmptyTable | table_without_rows_but_data_free | Info | Table is empty but has free space. It is probably used as a some form of queue. | +| ReservedKeywords | table_without_rows_but_data_free.name | Concern | `name` is a reserved keyword in MySQL 8.0. | +| | | | Avoid using reserved words as a column name. | +| | | | Reference: https://dev.mysql.com/doc/refman/8.0/en/keywords.html | ++----------------------+------------------------------------------+----------+----------------------------------------------------------------------------------+ +``` + +## Contributions + +This project adopts the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md) for contributions. + +Feel free to open an issue if you find any problems or have any suggestions or requests. \ No newline at end of file diff --git a/bin/cadfael b/bin/cadfael new file mode 100755 index 0000000..408dd2e --- /dev/null +++ b/bin/cadfael @@ -0,0 +1,14 @@ +#!/usr/bin/env php +add(new AboutCommand()); +$application->add(new RunCommand()); +$application->run(); \ No newline at end of file diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..55258c5 --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +./vendor/bin/phpstan analyse -l 6 src && \ + ./vendor/bin/phpcs -sw --standard=PSR2 src && \ + ./vendor/bin/phpunit diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b870ad9 --- /dev/null +++ b/composer.json @@ -0,0 +1,36 @@ +{ + "name": "cadfael/cadfael", + "description": "Tool for performing static analysis on databases.", + "type": "library", + "license": "GPL-3.0-or-later", + "authors": [ + { + "name": "Thomas Shone", + "email": "xsist10@gmail.com" + } + ], + "minimum-stability": "stable", + "require": { + "php": "^7.4", + "doctrine/dbal": "^2.10", + "symfony/console": "^5.0" + }, + "require-dev": { + "squizlabs/php_codesniffer": "^3.5", + "phpunit/phpunit": "^9.1", + "phpstan/phpstan": "^0.12.0@dev" + }, + "autoload": { + "psr-4": { + "Cadfael\\" : "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Cadfael\\Tests\\": "tests/" + } + }, + "bin": [ + "bin/cadfael" + ] +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..513f5b2 --- /dev/null +++ b/composer.lock @@ -0,0 +1,2897 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "0df3d20e842b73bc9fced29f0825db6e", + "packages": [ + { + "name": "doctrine/cache", + "version": "1.10.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "13e3381b25847283a91948d04640543941309727" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/13e3381b25847283a91948d04640543941309727", + "reference": "13e3381b25847283a91948d04640543941309727", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "alcaeus/mongo-php-adapter": "^1.1", + "doctrine/coding-standard": "^6.0", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^7.0", + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2020-07-07T18:54:01+00:00" + }, + { + "name": "doctrine/dbal", + "version": "2.10.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "aab745e7b6b2de3b47019da81e7225e14dcfdac8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/aab745e7b6b2de3b47019da81e7225e14dcfdac8", + "reference": "aab745e7b6b2de3b47019da81e7225e14dcfdac8", + "shasum": "" + }, + "require": { + "doctrine/cache": "^1.0", + "doctrine/event-manager": "^1.0", + "ext-pdo": "*", + "php": "^7.2" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "jetbrains/phpstorm-stubs": "^2019.1", + "nikic/php-parser": "^4.4", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^8.4.1", + "symfony/console": "^2.0.5|^3.0|^4.0|^5.0", + "vimeo/psalm": "^3.11" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.10.x-dev", + "dev-develop": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "lib/Doctrine/DBAL" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlanywhere", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2020-04-20T17:19:26+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "629572819973f13486371cb611386eb17851e85c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/629572819973f13486371cb611386eb17851e85c", + "reference": "629572819973f13486371cb611386eb17851e85c", + "shasum": "" + }, + "require": { + "php": "^7.1" + }, + "conflict": { + "doctrine/common": "<2.9@dev" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "lib/Doctrine/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "time": "2019-11-10T09:48:07+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "symfony/console", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "34ac555a3627e324b660e318daa07572e1140123" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/34ac555a3627e324b660e318daa07572e1140123", + "reference": "34ac555a3627e324b660e318daa07572e1140123", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2", + "symfony/string": "^5.1" + }, + "conflict": { + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", + "symfony/lock": "<4.4", + "symfony/process": "<4.4" + }, + "provide": { + "psr/log-implementation": "1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0", + "symfony/lock": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0", + "symfony/var-dumper": "^4.4|^5.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/lock": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-15T12:59:21+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.18.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.18.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b740103edbdcc39602239ee8860f0f45a8eb9aa5", + "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.18.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.18.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-php73", + "version": "v1.18.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca", + "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.18.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "shasum": "" + }, + "require": { + "php": ">=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/58c7475e5457c5492c26cc740cc0ad7464be9442", + "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.0" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-06T13:23:11+00:00" + }, + { + "name": "symfony/string", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "ac70459db781108db7c6d8981dd31ce0e29e3298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/ac70459db781108db7c6d8981dd31ce0e29e3298", + "reference": "ac70459db781108db7c6d8981dd31ce0e29e3298", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "~1.15" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony String component", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-11T12:16:36+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^6.0", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-05-29T17:27:14+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.10.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "replace": { + "myclabs/deep-copy": "self.version" + }, + "require-dev": { + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-06-29T13:22:24+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^2.0", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2018-07-08T19:23:20+00:00" + }, + { + "name": "phar-io/version", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2018-07-08T19:19:57+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "shasum": "" + }, + "require": { + "ext-filter": "^7.1", + "php": "^7.2", + "phpdocumentor/reflection-common": "^2.0", + "phpdocumentor/type-resolver": "^1.0", + "webmozart/assert": "^1" + }, + "require-dev": { + "doctrine/instantiator": "^1", + "mockery/mockery": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "account@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "time": "2020-02-22T12:28:44+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2020-06-27T10:12:23+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2", + "php": "^7.2", + "phpdocumentor/reflection-docblock": "^5.0", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2020-07-08T12:44:21+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "593d057f083cf5f633cdb308aa012b0aa395f5ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/593d057f083cf5f633cdb308aa012b0aa395f5ce", + "reference": "593d057f083cf5f633cdb308aa012b0aa395f5ce", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpstan", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2020-07-15T17:58:23+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ca6647ffddd2add025ab3f21644a441d7c146cdc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca6647ffddd2add025ab3f21644a441d7c146cdc", + "reference": "ca6647ffddd2add025ab3f21644a441d7c146cdc", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlwriter": "*", + "php": "^7.3", + "phpunit/php-file-iterator": "^3.0", + "phpunit/php-text-template": "^2.0", + "phpunit/php-token-stream": "^4.0", + "sebastian/code-unit-reverse-lookup": "^2.0", + "sebastian/environment": "^5.0", + "sebastian/version": "^3.0", + "theseer/tokenizer": "^1.1.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-pcov": "*", + "ext-xdebug": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-05-23T08:02:54+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/25fefc5b19835ca653877fe081644a3f8c1d915e", + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-11T05:18:21+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f6eedfed1085dd1f4c599629459a0277d25f9a66", + "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:53:53+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:55:37+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/cc49734779cbb302bf51a44297dab8c4bbf941e7", + "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:58:13+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5672711b6b07b14d5ab694e700c62eeb82fcf374", + "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-27T06:36:25+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.2.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "1c6a9e4312e209e659f1fce3ce88dd197c2448f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/1c6a9e4312e209e659f1fce3ce88dd197c2448f6", + "reference": "1c6a9e4312e209e659f1fce3ce88dd197c2448f6", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.9.5", + "phar-io/manifest": "^1.0.3", + "phar-io/version": "^2.0.1", + "php": "^7.3", + "phpspec/prophecy": "^1.10.3", + "phpunit/php-code-coverage": "^8.0.2", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-invoker": "^3.0.2", + "phpunit/php-text-template": "^2.0.2", + "phpunit/php-timer": "^5.0.1", + "sebastian/code-unit": "^1.0.5", + "sebastian/comparator": "^4.0.3", + "sebastian/diff": "^4.0.1", + "sebastian/environment": "^5.1.2", + "sebastian/exporter": "^4.0.2", + "sebastian/global-state": "^4.0", + "sebastian/object-enumerator": "^4.0.2", + "sebastian/resource-operations": "^3.0.2", + "sebastian/type": "^2.1.1", + "sebastian/version": "^3.0.1" + }, + "require-dev": { + "ext-pdo": "*", + "phpspec/prophecy-phpunit": "^2.0" + }, + "suggest": { + "ext-soap": "*", + "ext-xdebug": "*" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ], + "files": [ + "src/Framework/Assert/Functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-13T17:55:55+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "c1e2df332c905079980b119c4db103117e5e5c90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/c1e2df332c905079980b119c4db103117e5e5c90", + "reference": "c1e2df332c905079980b119c4db103117e5e5c90", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:50:45+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ee51f9bb0c6d8a43337055db3120829fa14da819", + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:04:00+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:05:46+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-30T04:46:02+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:07:24+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "571d721db4aec847a0e59690b954af33ebf9f023" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/571d721db4aec847a0e59690b954af33ebf9f023", + "reference": "571d721db4aec847a0e59690b954af33ebf9f023", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:08:55+00:00" + }, + { + "name": "sebastian/global-state", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bdb1e7c79e592b8c82cb1699be3c8743119b8a72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bdb1e7c79e592b8c82cb1699be3c8743119b8a72", + "reference": "bdb1e7c79e592b8c82cb1699be3c8743119b8a72", + "shasum": "" + }, + "require": { + "php": "^7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2020-02-07T06:11:37+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/074fed2d0a6d08e1677dd8ce9d32aecb384917b8", + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:11:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "127a46f6b057441b201253526f81d5406d6c7840" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/127a46f6b057441b201253526f81d5406d6c7840", + "reference": "127a46f6b057441b201253526f81d5406d6c7840", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:12:55+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/062231bf61d2b9448c4fa5a7643b5e1829c11d63", + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:14:17+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "0653718a5a629b065e91f774595267f8dc32e213" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0653718a5a629b065e91f774595267f8dc32e213", + "reference": "0653718a5a629b065e91f774595267f8dc32e213", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:16:22+00:00" + }, + { + "name": "sebastian/type", + "version": "2.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/86991e2b33446cd96e648c18bcdb1e95afb2c05a", + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-05T08:31:53+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/626586115d0ed31cb71483be55beb759b5af5a3c", + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:18:43+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.5.5", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/73e2e7f57d958e7228fce50dc0c61f58f017f9f6", + "reference": "73e2e7f57d958e7228fce50dc0c61f58f017f9f6", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2020-04-17T01:09:41+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "75a63c33a8577608444246075ea0af0d052e452a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.1", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0 || ^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.36 || ^7.5.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2020-07-08T17:02:28+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "phpstan/phpstan": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^7.4" + }, + "platform-dev": [], + "plugin-api-version": "1.1.0" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..c621caf --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,24 @@ + + + + + ./tests + + + + + + + + + + src/Engine + + + \ No newline at end of file diff --git a/src/Cli/Command/AboutCommand.php b/src/Cli/Command/AboutCommand.php new file mode 100644 index 0000000..e077f53 --- /dev/null +++ b/src/Cli/Command/AboutCommand.php @@ -0,0 +1,32 @@ +setDescription('About the Cadfael CLI tool.') + + // the full command description shown when running the command with + // the "--help" option + ->setHelp('This command provides a detailed explanation of what the Cadfael tool is for.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + // outputs multiple lines to the console (adding "\n" at the end of each line) + $output->writeln("About Information"); + + return Command::SUCCESS; + } +} diff --git a/src/Cli/Command/RunCommand.php b/src/Cli/Command/RunCommand.php new file mode 100644 index 0000000..9f0bf5e --- /dev/null +++ b/src/Cli/Command/RunCommand.php @@ -0,0 +1,139 @@ + '', + 2 => '', + 3 => '', + 4 => '', + 5 => '', + ]; + + protected function renderStatus(Report $report): string + { + return self::STATUS_COLOUR[$report->getStatus()]. $report->getStatusLabel() . ""; + } + + protected function configure(): void + { + $this + // the short description shown while running "php bin/console list" + ->setDescription('Run a collection of checks against a database.') + + ->addOption('host', null, InputOption::VALUE_REQUIRED, 'The host of the database.', 'localhost') + ->addOption('port', 'p', InputOption::VALUE_REQUIRED, 'The port of the database.', 3306) + ->addOption('username', 'u', InputOption::VALUE_REQUIRED, 'The username of the database.', 'root') + ->addArgument('schema', InputArgument::REQUIRED, 'The schema to scan.') + // the full command description shown when running the command with + // the "--help" option +// ->setHelp('.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln('Cadfael CLI Tool'); + $output->writeln(''); + + $output->writeln('Host: ' . $input->getOption('host') . ':' . $input->getOption('port')); + $output->writeln('User: ' . $input->getOption('username')); + $output->writeln(''); + + // outputs multiple lines to the console (adding "\n" at the end of each line) + $output->writeln("Attempting to scan schema " . $input->getArgument('schema') . ""); + + $question = new Question('What is the database password? '); + $question->setHidden(true); + $question->setHiddenFallback(false); + + $helper = $this->getHelper('question'); + $password = $helper->ask($input, $output, $question); + + $connectionParams = array( + 'dbname' => $input->getArgument('schema'), + 'user' => $input->getOption('username'), + 'password' => $password, + 'host' => $input->getOption('host'), + 'driver' => 'pdo_mysql', + ); + $connection = DriverManager::getConnection($connectionParams); + $factory = new Factory($connection); + + $table = new Table($output); + $table->setHeaders(['Check', 'Entity', 'Status', 'Message']); + $checks = [ + new MustHavePrimaryKey(), + new SaneInnoDbPrimaryKey(), + new EmptyTable(), + new AutoIncrementCapacity(), + new RedundantIndexes(), + new ReservedKeywords(), + new SaneAutoIncrement(), + new CorrectUtf8Encoding(), + ]; + + foreach ($factory->getTables("tests") as $entity) { + foreach ($checks as $check) { + if ($check->supports($entity)) { + $report = $check->run($entity); + if (!is_null($report) && $report->getStatus() != Report::STATUS_OK) { + $table->addRow([ + $report->getCheckLabel(), + $report->getEntity(), + $this->renderStatus($report), + implode("\n", $report->getMessages()) + ]); + } + } + + foreach ($entity->getColumns() as $column) { + if ($check->supports($column)) { + $report = $check->run($column); + if (!is_null($report) && $report->getStatus() != Report::STATUS_OK) { + $table->addRow([ + $report->getCheckLabel(), + $report->getEntity(), + $this->renderStatus($report), + implode("\n", $report->getMessages()) + ]); + } + } + } + } + } + + $table->setColumnMaxWidth(0, 22); + $table->setColumnMaxWidth(1, 40); + $table->setColumnMaxWidth(2, 8); + $table->setColumnMaxWidth(3, 80); + $table->render(); + $output->writeln(''); + + return Command::SUCCESS; + } +} diff --git a/src/Engine/Check.php b/src/Engine/Check.php new file mode 100644 index 0000000..5a5ee2c --- /dev/null +++ b/src/Engine/Check.php @@ -0,0 +1,21 @@ +information_schema->character_set_name)) { + return null; + } + + if ($entity->information_schema->character_set_name !== 'utf8') { + return new Report( + $this, + $entity, + Report::STATUS_OK + ); + } + + $reference = "https://www.eversql.com/mysql-utf8-vs-utf8mb4-whats-the-difference-between-utf8-and-utf8mb4/"; + return new Report( + $this, + $entity, + Report::STATUS_CONCERN, + [ + "Character set should be utf8mb2 not utf8.", + "Reference: $reference" + ] + ); + } +} diff --git a/src/Engine/Check/MySQL/Column/ReservedKeywords.php b/src/Engine/Check/MySQL/Column/ReservedKeywords.php new file mode 100644 index 0000000..bef1d5b --- /dev/null +++ b/src/Engine/Check/MySQL/Column/ReservedKeywords.php @@ -0,0 +1,746 @@ +getName()), self::RESERVED_KEYWORDS)) { + return null; + } + + return new Report( + $this, + $entity, + Report::STATUS_CONCERN, + [ + "`" . $entity->getName() . "` is a reserved keyword in MySQL 8.0.", + "Avoid using reserved words as a column name.", + "Reference: https://dev.mysql.com/doc/refman/8.0/en/keywords.html", + ] + ); + } +} diff --git a/src/Engine/Check/MySQL/Column/SaneAutoIncrement.php b/src/Engine/Check/MySQL/Column/SaneAutoIncrement.php new file mode 100644 index 0000000..df4a96e --- /dev/null +++ b/src/Engine/Check/MySQL/Column/SaneAutoIncrement.php @@ -0,0 +1,59 @@ +isAutoIncrementing() + && !$entity->isVirtual() + && !$entity->getTable()->isVirtual(); + } + + public function run($entity): ?Report + { + $messages = []; + + // Auto increment should be an unsigned integer type field + if (!$entity->isInteger() || $entity->isSigned()) { + $messages[] = 'This field should be an unsigned integer type.'; + } + // It should be the primary key + if (!$entity->isPartOfPrimaryKey()) { + $messages[] = 'This field should be set as the primary key.'; + } else { + // If should be the ONLY part of the primary key + $primary_columns = $entity->getTable()->getPrimaryKeys(); + + if (count($primary_columns) > 1) { + $messages[] = 'This field should be a non-compound primary key.'; + } + } + + if (count($messages)) { + return new Report( + $this, + $entity, + Report::STATUS_WARNING, + $messages + ); + } + + // Otherwise this column is fine + return new Report( + $this, + $entity, + Report::STATUS_OK + ); + } +} diff --git a/src/Engine/Check/MySQL/Table/AutoIncrementCapacity.php b/src/Engine/Check/MySQL/Table/AutoIncrementCapacity.php new file mode 100644 index 0000000..2ca4373 --- /dev/null +++ b/src/Engine/Check/MySQL/Table/AutoIncrementCapacity.php @@ -0,0 +1,84 @@ +schema_auto_increment_columns) + && !$entity->isVirtual(); + } + + public function run($entity): ?Report + { + $auto_increment = $entity->schema_auto_increment_columns; + + // It is possible that we don't have access to the `sys` schema with the credentials supplied. + if (is_null($auto_increment)) { + throw new MissingSysData("Missing information from `sys.schema_auto_increment_columns`"); + } + + $percentage = $auto_increment->auto_increment_ratio * 100; + $percentage_used = sprintf("%0.2f%%", $percentage); + + $data = [ + 'total' => $auto_increment->max_value, + 'used' => $auto_increment->auto_increment ?? 0, + 'available' => $auto_increment->max_value - $auto_increment->auto_increment, + 'percentage' => $percentage_used, + ]; + + // Some results from sys.schema_auto_increment_columns contain undef + // values instead of 0 for new tables. We'll just skip these are they are + // already well within threshold (since it means they're empty). + if (!$auto_increment->auto_increment) { + return new Report( + $this, + $entity, + Report::STATUS_OK, + [], + $data + ); + } + + $messages = []; + + $status = Report::STATUS_OK; + if ($percentage >= 60) { + $status = Report::STATUS_WARNING; + } + + if ($percentage >= 80) { + $status = Report::STATUS_CRITICAL; + } + + if ( + // We only care about entries that throw an error + $status !== Report::STATUS_OK + // If they have a small capacity then they aren't likely needed to grow + && $auto_increment->max_value <= 1024 + ) { + $status = Report::STATUS_WARNING; + $messages[] = "Your auto increment column has a small capacity so it may be intentional."; + } + + return new Report( + $this, + $entity, + $status, + $messages, + $data + ); + } +} diff --git a/src/Engine/Check/MySQL/Table/EmptyTable.php b/src/Engine/Check/MySQL/Table/EmptyTable.php new file mode 100644 index 0000000..32480c9 --- /dev/null +++ b/src/Engine/Check/MySQL/Table/EmptyTable.php @@ -0,0 +1,46 @@ +isVirtual(); + } + + public function run($entity): ?Report + { + if (!empty($entity->information_schema->table_rows)) { + return new Report( + $this, + $entity, + Report::STATUS_OK + ); + } + + if ($entity->information_schema->data_free > 0 || !empty($entity->information_schema->auto_increment)) { + return new Report( + $this, + $entity, + Report::STATUS_INFO, + [ "Table is empty but has free space. It is probably used as a some form of queue." ] + ); + } + + return new Report( + $this, + $entity, + Report::STATUS_WARNING, + [ "Table contains no records." ] + ); + } +} diff --git a/src/Engine/Check/MySQL/Table/MustHavePrimaryKey.php b/src/Engine/Check/MySQL/Table/MustHavePrimaryKey.php new file mode 100644 index 0000000..091eebe --- /dev/null +++ b/src/Engine/Check/MySQL/Table/MustHavePrimaryKey.php @@ -0,0 +1,38 @@ +isVirtual(); + } + + public function run($entity): ?Report + { + $messages = [ + "Table must have a PRIMARY KEY", + "Reference: https://federico-razzoli.com/why-mysql-tables-need-a-primary-key.", + ]; + if ($entity->information_schema->engine === 'InnoDB') { + $messages[] = "MySQL 8 replication will break if you have InnoDB tables without a PRIMARY KEY."; + } + return new Report( + $this, + $entity, + count($entity->getPrimaryKeys()) > 0 + ? Report::STATUS_OK + : Report::STATUS_CRITICAL, + $messages + ); + } +} diff --git a/src/Engine/Check/MySQL/Table/RedundantIndexes.php b/src/Engine/Check/MySQL/Table/RedundantIndexes.php new file mode 100644 index 0000000..30b72e8 --- /dev/null +++ b/src/Engine/Check/MySQL/Table/RedundantIndexes.php @@ -0,0 +1,50 @@ +isVirtual(); + } + + public function run($entity): ?Report + { + if (!count($entity->schema_redundant_indexes)) { + return new Report( + $this, + $entity, + Report::STATUS_OK, + ["No redundant indexes found."] + ); + } + + $messages = []; + foreach ($entity->schema_redundant_indexes as $redundant_index) { + $messages[] = sprintf( + "Redundant index %s (superseded by %s).", + $redundant_index->redundant_index_name, + $redundant_index->dominant_index_name + ); + } + $messages[] = "A redundant index can probably drop it (unless it's a UNIQUE, in which case the dominant index " + . "might be a better candidate for reworking)."; + $messages[] = "Reference: https://dev.mysql.com/doc/refman/5.7/en/sys-schema-redundant-indexes.html"; + + return new Report( + $this, + $entity, + Report::STATUS_CONCERN, + $messages + ); + } +} diff --git a/src/Engine/Check/MySQL/Table/SaneInnoDbPrimaryKey.php b/src/Engine/Check/MySQL/Table/SaneInnoDbPrimaryKey.php new file mode 100644 index 0000000..b527edd --- /dev/null +++ b/src/Engine/Check/MySQL/Table/SaneInnoDbPrimaryKey.php @@ -0,0 +1,76 @@ +information_schema) + && $entity->information_schema->engine === 'InnoDB'; + } + + public function run($entity): ?Report + { + $primary_keys = $entity->getPrimaryKeys(); + // If the table doesn't have a PRIMARY KEY, we skip this test + if (!count($primary_keys)) { + return null; + } + + // Find out the size of our primary key (bytes) + $primary_key_size = array_reduce($primary_keys, function ($size, $column) { + return $size + $column->getStorageByteSize(); + }); + + // Get a count of all the indexes that are not the PRIMARY KEY + $index_count = count(array_filter($entity->getIndexes(), function ($index) { + return $index->getName() !== "PRIMARY KEY"; + })); + + // If our primary key is a sane size, or if we don't have any other indexes, we can exit now + if ($primary_key_size <= 8 || $index_count == 0) { + return new Report( + $this, + $entity, + Report::STATUS_OK + ); + } + + // If these is, lets work out if it's more space efficient to create a UNIQUE + // KEY of the PRIMARY KEY and add an AUTO_INCREMENT field (4 bytes) instead. + $current_cost = $primary_key_size + ($index_count * $primary_key_size); + $unique_key_cost = 4 + ($primary_key_size + 4) + ($index_count * 4); + + + // If the current cost is less than a unique constraint, then it's OK + if ($current_cost <= $unique_key_cost) { + return new Report( + $this, + $entity, + Report::STATUS_OK + ); + } + + return new Report( + $this, + $entity, + Report::STATUS_WARNING, + [ + "In InnoDB tables, the PRIMARY KEY is appended to other indexes.", + "If the PRIMARY KEY is big, other indexes will use more space.", + "Maybe turn your PRIMARY KEY into UNIQUE and add an auto_increment PRIMARY KEY.", + "Reference: https://dev.mysql.com/doc/refman/5.7/en/innodb-index-types.html" + ] + ); + } +} diff --git a/src/Engine/Entity.php b/src/Engine/Entity.php new file mode 100644 index 0000000..c1fbbd4 --- /dev/null +++ b/src/Engine/Entity.php @@ -0,0 +1,17 @@ +name; + } + + public function setTable(Table $table): void + { + $this->table = $table; + } + + public function __toString(): string + { + return (string)$this->table . "." . $this->name; + } + + public function getTable(): Table + { + return $this->table; + } + + abstract public function isVirtual(): bool; + abstract public function isPartOfPrimaryKey() : bool; + abstract public function isSigned(): bool; + abstract public function isAutoIncrementing(): bool; + abstract public function isInteger(): bool; + abstract public function isNumeric(): bool; + abstract public function getStorageByteSize(): float; +} diff --git a/src/Engine/Entity/Index.php b/src/Engine/Entity/Index.php new file mode 100644 index 0000000..1eb13c1 --- /dev/null +++ b/src/Engine/Entity/Index.php @@ -0,0 +1,52 @@ + + */ + protected array $columns = []; + + public function __construct(string $name) + { + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } + + public function setTable(Table $table): void + { + $this->table = $table; + } + + /** + * Is this entity virtual (generated rather than stored on disk)? + * + * @return bool + */ + public function isVirtual(): bool + { + return false; + } + + public function setColumns(Column ...$columns): void + { + $this->columns = $columns; + } + + public function __toString(): string + { + return (string)$this->table . "." . $this->name; + } +} diff --git a/src/Engine/Entity/MySQL/Column.php b/src/Engine/Entity/MySQL/Column.php new file mode 100644 index 0000000..88aa7dd --- /dev/null +++ b/src/Engine/Entity/MySQL/Column.php @@ -0,0 +1,190 @@ + $schema This is a raw record from information_schema.COLUMN + * @return Column + */ + public static function createFromInformationSchema(array $schema) + { + $column = new Column(); + $column->name = $schema['COLUMN_NAME']; + $column->information_schema = InformationSchema::createFromInformationSchema($schema); + + return $column; + } + + public function isPartOfPrimaryKey() : bool + { + return $this->information_schema->column_key === 'PRI'; + } + + public function isVirtual(): bool + { + // In MySQL, generated columns contain a generation expression in the information schema + // COLUMN.generation_expression field. + return !empty($this->information_schema->generation_expression); + } + + public function isSigned(): bool + { + return $this->isNumeric() + && strpos($this->information_schema->column_type, 'unsigned') === false; + } + + public function isAutoIncrementing(): bool + { + return strpos($this->information_schema->extra, 'auto_increment') !== false; + } + + public function isInteger(): bool + { + return in_array($this->information_schema->data_type, ['tinyint', 'smallint', 'mediumint', 'int', 'bigint' ]); + } + + public function isNumeric(): bool + { + return $this->isInteger() + || in_array($this->information_schema->data_type, [ 'bit', 'decimal', 'double', 'float' ]); + } + + public function getStorageByteSize(): float + { + $size = [ + 'tinytext ' => [ + 'body' => 255, + 'header' => 1, + ], + 'text ' => [ + 'body' => 65535, + 'header' => 2, + ], + 'blob ' => [ + 'body' => 65535, + 'header' => 2, + ], + 'binary' => [ + 'body' => $this->information_schema->character_maximum_length, + 'header' => 0, + ], + 'bit' => [ + 'body' => ceil(($this->information_schema->numeric_precision + 7) / 8), + 'header' => 0, + ], + 'varbinary' => [ + 'body' => $this->information_schema->character_maximum_length, + 'header' => 1, + ], + 'mediumblob' => [ + 'body' => 16777215, + 'header' => 3, + ], + 'mediumtext' => [ + 'body' => 16777215, + 'header' => 3, + ], + 'longtext ' => [ + 'body' => 4294967295, + 'header' => 4, + ], + 'json ' => [ + 'body' => 4294967295, + 'header' => 4, + ], + 'longblob ' => [ + 'body' => 4294967295, + 'header' => 4, + ], + 'char' => [ + 'body' => $this->information_schema->character_maximum_length, + 'header' => 0 + ], + 'varchar' => [ + 'body' => $this->information_schema->character_maximum_length, + 'header' => 1 + ], + 'tinyint' => [ + 'body' => 1, + 'header' => 0 + ], + 'smallint' => [ + 'body' => 2, + 'header' => 0 + ], + 'mediumint' => [ + 'body' => 3, + 'header' => 0 + ], + 'int' => [ + 'body' => 4, + 'header' => 0 + ], + 'bigint' => [ + 'body' => 8, + 'header' => 0 + ], + 'float' => [ + 'body' => 4, + 'header' => 0 + ], + 'double' => [ + 'body' => 8, + 'header' => 0 + ], + 'decimal' => [ + 'body' => $this->information_schema->numeric_precision, + 'header' => 2 + ], + 'date' => [ + 'body' => 3, + 'header' => 0 + ], + 'datetime' => [ + 'body' => 8, + 'header' => 0 + ], + 'timestamp' => [ + 'body' => 4, + 'header' => 0 + ], + 'time' => [ + 'body' => 3, + 'header' => 0 + ], + 'year' => [ + 'body' => 1, + 'header' => 0 + ], + 'enum' => [ + 'body' => 1, + 'header' => 0 + ], + 'set' => [ + 'body' => 1, + 'header' => 0 + ], + ]; + + if (!empty($size[$this->information_schema->data_type])) { + return $size[$this->information_schema->data_type]['body'] + + $size[$this->information_schema->data_type]['header']; + } + + throw new UnknownColumnType($this->information_schema->data_type . " is an unknown data type."); + } +} diff --git a/src/Engine/Entity/MySQL/Column/InformationSchema.php b/src/Engine/Entity/MySQL/Column/InformationSchema.php new file mode 100644 index 0000000..e4d787f --- /dev/null +++ b/src/Engine/Entity/MySQL/Column/InformationSchema.php @@ -0,0 +1,106 @@ + $schema This is a raw record from information_schema.COLUMN + * @return InformationSchema + */ + public static function createFromInformationSchema(array $schema) + { + $informationSchema = new InformationSchema(); + $informationSchema->ordinal_position = (int)$schema['ORDINAL_POSITION']; + $informationSchema->default = $schema['COLUMN_DEFAULT']; + $informationSchema->is_nullable = $schema['IS_NULLABLE'] === 'YES'; + $informationSchema->column_type = $schema['COLUMN_TYPE']; + $informationSchema->data_type = $schema['DATA_TYPE']; + $informationSchema->column_key = $schema['COLUMN_KEY']; + $informationSchema->character_maximum_length = (int)$schema['CHARACTER_MAXIMUM_LENGTH']; + $informationSchema->character_octet_length = (int)$schema['CHARACTER_OCTET_LENGTH']; + $informationSchema->numeric_precision = (int)$schema['NUMERIC_PRECISION']; + $informationSchema->datetime_precision = (int)$schema['DATETIME_PRECISION']; + $informationSchema->character_set_name = $schema['CHARACTER_SET_NAME']; + $informationSchema->collation_name = $schema['COLLATION_NAME']; + $informationSchema->extra = $schema['EXTRA']; + $informationSchema->privileges = $schema['PRIVILEGES']; + $informationSchema->column_comment = $schema['COLUMN_COMMENT']; + $informationSchema->generation_expression = $schema['GENERATION_EXPRESSION']; + return $informationSchema; + } +} diff --git a/src/Engine/Entity/MySQL/Table.php b/src/Engine/Entity/MySQL/Table.php new file mode 100644 index 0000000..ba08f35 --- /dev/null +++ b/src/Engine/Entity/MySQL/Table.php @@ -0,0 +1,66 @@ + + */ + public array $schema_redundant_indexes = []; + + /** + * @param array $schema This is a raw record from information_schema.TABLE + * @return Table + */ + public static function createFromInformationSchema(array $schema): Table + { + $table = new Table($schema['TABLE_SCHEMA'], $schema['TABLE_NAME']); + $table->information_schema = InformationSchema::createFromInformationSchema($schema); + + return $table; + } + + /** + * @codeCoverageIgnore + * Skip coverage as this is a basic accessor. Remove if the accessor behaviour becomes more complicated. + */ + public function setSchemaAutoIncrementColumns(SchemaAutoIncrementColumns $schema_auto_increment_columns): void + { + $this->schema_auto_increment_columns = $schema_auto_increment_columns; + } + + /** + * @codeCoverageIgnore + * Skip coverage as this is a basic accessor. Remove if the accessor behaviour becomes more complicated. + */ + public function setSchemaRedundantIndexes(SchemaRedundantIndexes ...$schema_redundant_indexes): void + { + $this->schema_redundant_indexes = $schema_redundant_indexes; + } + + public function isVirtual(): bool + { + // If we don't have an information_schema, we'll have to guess + if (empty($this->information_schema)) { + return true; + } + + return // The blackhole engine acts just like writing to /dev/null + $this->information_schema->engine === 'BLACKHOLE' + // System view tables are virtual + || $this->information_schema->table_type === 'SYSTEM VIEW' + // Views are virtual + || $this->information_schema->table_type === 'VIEW'; + } +} diff --git a/src/Engine/Entity/MySQL/Table/InformationSchema.php b/src/Engine/Entity/MySQL/Table/InformationSchema.php new file mode 100644 index 0000000..19bc233 --- /dev/null +++ b/src/Engine/Entity/MySQL/Table/InformationSchema.php @@ -0,0 +1,62 @@ + $schema This is a raw record from information_schema.TABLE + * @return InformationSchema + */ + public static function createFromInformationSchema(array $schema): InformationSchema + { + $informationSchema = new InformationSchema(); + $informationSchema->table_type = $schema['TABLE_TYPE']; + $informationSchema->engine = $schema['ENGINE']; + $informationSchema->version = $schema['VERSION']; + $informationSchema->row_format = $schema['ROW_FORMAT']; + $informationSchema->table_rows = (int)$schema['TABLE_ROWS']; + $informationSchema->avg_row_length = (int)$schema['AVG_ROW_LENGTH']; + $informationSchema->data_length = (int)$schema['DATA_LENGTH']; + $informationSchema->max_data_length = (int)$schema['MAX_DATA_LENGTH']; + $informationSchema->data_free = (int)$schema['DATA_FREE']; + $informationSchema->auto_increment = $schema['AUTO_INCREMENT']; + $informationSchema->create_time = $schema['CREATE_TIME']; + $informationSchema->update_time = $schema['UPDATE_TIME']; + $informationSchema->check_time = $schema['CHECK_TIME']; + $informationSchema->table_collation = $schema['TABLE_COLLATION']; + $informationSchema->checksum = $schema['CHECKSUM']; + $informationSchema->create_options = $schema['CREATE_OPTIONS']; + $informationSchema->table_comment = $schema['TABLE_COMMENT']; + return $informationSchema; + } +} diff --git a/src/Engine/Entity/MySQL/Table/SchemaAutoIncrementColumns.php b/src/Engine/Entity/MySQL/Table/SchemaAutoIncrementColumns.php new file mode 100644 index 0000000..da600df --- /dev/null +++ b/src/Engine/Entity/MySQL/Table/SchemaAutoIncrementColumns.php @@ -0,0 +1,47 @@ + $schema This is a raw record from sys.schema_auto_increment_columns + * @return SchemaAutoIncrementColumns + */ + public static function createFromSys(array $schema): SchemaAutoIncrementColumns + { + $schemaAutoIncrementColumns = new SchemaAutoIncrementColumns(); + $schemaAutoIncrementColumns->column_name = $schema['column_name']; + $schemaAutoIncrementColumns->data_type = $schema['data_type']; + $schemaAutoIncrementColumns->column_type = $schema['column_type']; + $schemaAutoIncrementColumns->is_signed = $schema['is_signed'] == '1'; + $schemaAutoIncrementColumns->is_unsigned = $schema['is_unsigned'] == '1'; + $schemaAutoIncrementColumns->max_value = (float)$schema['max_value']; + $schemaAutoIncrementColumns->auto_increment = (float)$schema['auto_increment']; + $schemaAutoIncrementColumns->auto_increment_ratio = (float)$schema['auto_increment_ratio']; + + return $schemaAutoIncrementColumns; + } +} diff --git a/src/Engine/Entity/MySQL/Table/SchemaRedundantIndexes.php b/src/Engine/Entity/MySQL/Table/SchemaRedundantIndexes.php new file mode 100644 index 0000000..ca28501 --- /dev/null +++ b/src/Engine/Entity/MySQL/Table/SchemaRedundantIndexes.php @@ -0,0 +1,55 @@ + + */ + public array $redundant_index_columns; + public int $redundant_index_non_unique; + public string $dominant_index_name; + /** + * @todo Updated to support Column instance + * @var array + */ + public array $dominant_index_columns; + public int $dominant_index_non_unique; + public int $subpart_exists; + public string $sql_drop_index; + + public function __construct() + { + } + + /** + * @param array $schema This is a raw record from sys.schema_redundant_indexes + * @return SchemaRedundantIndexes + */ + public static function createFromSys(array $schema): SchemaRedundantIndexes + { + $schemaRedundantIndexes = new SchemaRedundantIndexes(); + $schemaRedundantIndexes->redundant_index_name = $schema['redundant_index_name']; + $schemaRedundantIndexes->redundant_index_columns = explode(',', $schema['redundant_index_columns']); + $schemaRedundantIndexes->redundant_index_non_unique = (int)$schema['redundant_index_non_unique']; + $schemaRedundantIndexes->dominant_index_name = $schema['dominant_index_name']; + $schemaRedundantIndexes->dominant_index_columns = explode(',', $schema['dominant_index_columns']); + $schemaRedundantIndexes->dominant_index_non_unique = (int)$schema['dominant_index_non_unique']; + $schemaRedundantIndexes->subpart_exists = (int)$schema['subpart_exists']; + $schemaRedundantIndexes->sql_drop_index = $schema['sql_drop_index']; + + return $schemaRedundantIndexes; + } +} diff --git a/src/Engine/Entity/Table.php b/src/Engine/Entity/Table.php new file mode 100644 index 0000000..a16fe1c --- /dev/null +++ b/src/Engine/Entity/Table.php @@ -0,0 +1,79 @@ + + */ + protected array $columns = []; + /** + * @var array + */ + protected array $indexes = []; + + public function __construct(string $schema, string $name) + { + $this->name = $name; + $this->schema = $schema; + } + + public function setColumns(Column ...$columns): void + { + array_walk($columns, function ($column) { + $column->setTable($this); + }); + $this->columns = $columns; + } + + /** + * @return array + */ + public function getColumns(): array + { + return $this->columns ?? []; + } + + public function getName(): string + { + return $this->name; + } + + public function setIndexes(Index ...$indexes): void + { + array_walk($indexes, function ($index) { + $index->setTable($this); + }); + $this->indexes = $indexes; + } + + /** + * @return array + */ + public function getIndexes(): array + { + return $this->indexes ?? []; + } + + /** + * @return array + */ + public function getPrimaryKeys(): array + { + return array_filter($this->getColumns() ?? [], function ($column) { + return $column->isPartOfPrimaryKey(); + }); + } + + public function __toString(): string + { + return $this->name; + } +} diff --git a/src/Engine/Exception/InvalidStatus.php b/src/Engine/Exception/InvalidStatus.php new file mode 100644 index 0000000..a00892b --- /dev/null +++ b/src/Engine/Exception/InvalidStatus.php @@ -0,0 +1,9 @@ +> + */ + private $permissions = []; + + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + protected function hasPermission(string $schema, string $table): bool + { + if (!count($this->permissions)) { + // First we attempt to figure out what permissions the user has + $query = 'SHOW GRANTS FOR CURRENT_USER();'; + $this->connection->setFetchMode(FetchMode::NUMERIC); + $rows = $this->connection->fetchAll($query); + foreach ($rows as $permission) { + preg_match('/^GRANT .*?[ALL|SELECT].*? ON (.*?)\.(.*?) TO/', $permission[0], $matches); + if (count($matches)) { + $this->permissions[$matches[1]][$matches[2]] = true; + } + } + } + + return !empty($this->permissions[$schema][$table]) + || !empty($this->permissions[$schema]['*']) + || !empty($this->permissions['*']['*']); + } + + /** + * @throws MissingPermissions + */ + protected function checkRequiredPermissions(): void + { + $message = 'Required access to %s.%s missing for this user account.'; + $accesses = [ + [ + 'schema' => 'information_schema', + 'table' => 'TABLES', + ], + [ + 'schema' => 'information_schema', + 'table' => 'COLUMNS', + ], + [ + 'schema' => 'information_schema', + 'table' => 'STATISTICS', + ], + [ + 'schema' => 'sys', + 'table' => 'schema_auto_increment_columns', + ], + [ + 'schema' => 'sys', + 'table' => 'schema_redundant_indexes', + ], + ]; + + foreach ($accesses as $access) { + if (!$this->hasPermission($access['schema'], $access['table'])) { + throw new MissingPermissions(sprintf($message, $access['schema'], $access['table'])); + } + } + } + + /** + * @param string $database + * @return array + * @throws \Doctrine\DBAL\DBALException + */ + public function getTables(string $database): array + { + $this->checkRequiredPermissions(); + $this->connection->setFetchMode(FetchMode::ASSOCIATIVE); + + // Collect and generate all the tables + $query = 'SELECT * FROM information_schema.TABLES WHERE TABLE_SCHEMA=:database'; + $statement = $this->connection->prepare($query); + $statement->bindValue("database", $database); + $statement->execute(); + + $rows = $statement->fetchAll(); + $tables = []; + foreach ($rows as $row) { + $tables[] = Table::createFromInformationSchema($row); + } + + // Collect and generate all the columns + $query = 'SELECT * FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=:database'; + $statement = $this->connection->prepare($query); + $statement->bindValue("database", $database); + $statement->execute(); + + $rows = $statement->fetchAll(); + $columns = []; + foreach ($rows as $row) { + $column = Column::createFromInformationSchema($row); + $columns[$row['TABLE_NAME']][$row['COLUMN_NAME']] = $column; + } + + // Collect and generate all sys.* information + $query = 'SELECT * FROM sys.schema_auto_increment_columns WHERE table_schema=:database'; + $statement = $this->connection->prepare($query); + $statement->bindValue("database", $database); + $statement->execute(); + + $rows = $statement->fetchAll(); + $schemaAutoIncrementColumns = []; + foreach ($rows as $row) { + $schemaAutoIncrementColumns[$row['table_name']] = SchemaAutoIncrementColumns::createFromSys($row); + } + + $query = 'SELECT * FROM sys.schema_redundant_indexes WHERE table_schema=:database'; + $statement = $this->connection->prepare($query); + $statement->bindValue("database", $database); + $statement->execute(); + + $rows = $statement->fetchAll(); + $schemaRedundantIndexes = []; + foreach ($rows as $row) { + $schemaRedundantIndexes[$row['table_name']][] = SchemaRedundantIndexes::createFromSys($row); + } + + // Collect and generate all the indexes + $query = 'SELECT * FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=:database'; + $statement = $this->connection->prepare($query); + $statement->bindValue("database", $database); + $statement->execute(); + + $rows = $statement->fetchAll(); + $indexes = []; + foreach ($rows as $row) { + $col = $columns[$row['TABLE_NAME']][$row['COLUMN_NAME']]; + $indexes[$row['TABLE_NAME']][$row['INDEX_NAME']][$row['SEQ_IN_INDEX']] = $col; + } + + $table_indexes_objects = []; + foreach ($indexes as $table_name => $table_indexes) { + foreach ($table_indexes as $index_name => $index_columns) { + $index = new Index((string)$index_name); + $index->setColumns(...$index_columns); + $table_indexes_objects[$table_name][] = $index; + } + } + + foreach ($tables as $table) { + if (!empty($columns[$table->getName()])) { + $table->setColumns(...array_values($columns[$table->getName()])); + } + if (!empty($table_indexes_objects[$table->getName()])) { + $table->setIndexes(...array_values($table_indexes_objects[$table->getName()])); + } + if (!empty($schemaAutoIncrementColumns[$table->getName()])) { + $table->setSchemaAutoIncrementColumns($schemaAutoIncrementColumns[$table->getName()]); + } + if (!empty($schemaRedundantIndexes[$table->getName()])) { + $table->setSchemaRedundantIndexes(...$schemaRedundantIndexes[$table->getName()]); + } + } + + return $tables; + } +} diff --git a/src/Engine/Report.php b/src/Engine/Report.php new file mode 100644 index 0000000..b8db15d --- /dev/null +++ b/src/Engine/Report.php @@ -0,0 +1,105 @@ + 'Ok', + self::STATUS_INFO => 'Info', + self::STATUS_CONCERN => 'Concern', + self::STATUS_WARNING => 'Warning', + self::STATUS_CRITICAL => 'Critical', + ]; + + protected Check $check; + protected Entity $entity; + protected int $status; + /** + * @var array + */ + protected array $messages = []; + /** + * @var array + */ + protected array $data = []; + + /** + * Report constructor. + * @param Check $check + * @param Entity $entity + * @param int $status + * @param array $messages + * @param array $data + * @throws InvalidStatus + */ + public function __construct(Check $check, Entity $entity, int $status, array $messages = [], array $data = []) + { + if (!self::isValidStatus($status)) { + throw new InvalidStatus("$status is not a valid status code."); + } + + $this->check = $check; + $this->entity = $entity; + $this->status = $status; + $this->messages = $messages; + $this->data = $data; + } + + public static function isValidStatus(int $status): bool + { + return $status >= 1 && $status <= 5; + } + + public function getStatus(): int + { + return $this->status; + } + + public function getStatusLabel(): string + { + return self::STATUS_LABEL[$this->status]; + } + + public function getCheck(): Check + { + return $this->check; + } + + public function getCheckLabel(): string + { + $label = explode('\\', get_class($this->check)); + return array_pop($label) ?? 'unknown'; + } + + public function getEntity(): Entity + { + return $this->entity; + } + + /** + * @return array + */ + public function getMessages(): array + { + return $this->messages; + } + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } +} diff --git a/tests/Engine/Check/MySQL/BaseTest.php b/tests/Engine/Check/MySQL/BaseTest.php new file mode 100644 index 0000000..f09265b --- /dev/null +++ b/tests/Engine/Check/MySQL/BaseTest.php @@ -0,0 +1,72 @@ + "MOCK_CATALOG", + "TABLE_SCHEMA" => "MOCK_SCHEMA", + "TABLE_NAME" => "MOCK_TABLE", + "TABLE_TYPE" => "BASE TABLE", + "ENGINE" => "InnoDB", + "VERSION" => "10", + "ROW_FORMAT" => "Fixed", + "TABLE_ROWS" => 200, + "AVG_ROW_LENGTH" => 384, + "DATA_LENGTH" => 2311, + "MAX_DATA_LENGTH" => 16434816, + "INDEX_LENGTH" => 0, + "DATA_FREE" => 0, + "AUTO_INCREMENT" => null, + "CREATE_TIME" => "2020-05-30 11:29:56", + "UPDATE_TIME" => null, + "CHECK_TIME" => null, + "TABLE_COLLATION" => "utf8_general_ci", + "CHECKSUM" => null, + "CREATE_OPTIONS" => "", + "TABLE_COMMENT" => "", + ]; + + return Table::createFromInformationSchema(array_merge($base, $override)); + } + + protected function createEmptyTable(array $override = []): Table + { + return $this->createTable( + array_merge( + [ + "TABLE_ROWS" => null, + "DATA_LENGTH" => 0 + ], + $override + ) + ); + } + + protected function createVirtualTable(array $override = []): Table + { + return $this->createTable( + array_merge( + [ + "ENGINE" => "BLACKHOLE" + ], + $override + ) + ); + } +} \ No newline at end of file diff --git a/tests/Engine/Check/MySQL/Column/CorrectUtf8EncodingTest.php b/tests/Engine/Check/MySQL/Column/CorrectUtf8EncodingTest.php new file mode 100644 index 0000000..96bcf3a --- /dev/null +++ b/tests/Engine/Check/MySQL/Column/CorrectUtf8EncodingTest.php @@ -0,0 +1,54 @@ +nonCharacterColumn = $builder->name("nonCharacterColumn") + ->int() + ->generate(); + $this->nonCharacterColumn->setTable($this->createTable()); + + $this->correctCharacterEncodingColumn = $builder->name("correctCharacterEncodingColumn") + ->varchar() + ->character_encoding('utf8mb4') + ->generate(); + $this->correctCharacterEncodingColumn->setTable($this->createTable()); + + $this->incorrectCharacterEncodingColumn = $builder->name("incorrectCharacterEncodingColumn") + ->varchar() + ->character_encoding('utf8') + ->generate(); + $this->incorrectCharacterEncodingColumn->setTable($this->createTable()); + } + + public function testSupports() + { + $check = new CorrectUtf8Encoding(); + + $this->assertTrue($check->supports($this->nonCharacterColumn)); + $this->assertTrue($check->supports($this->correctCharacterEncodingColumn)); + $this->assertTrue($check->supports($this->incorrectCharacterEncodingColumn)); + } + + public function testRun() + { + $check = new CorrectUtf8Encoding(); + $this->assertNull($check->run($this->nonCharacterColumn)); + $this->assertEquals(Report::STATUS_OK, $check->run($this->correctCharacterEncodingColumn)->getStatus()); + $this->assertEquals(Report::STATUS_CONCERN, $check->run($this->incorrectCharacterEncodingColumn)->getStatus()); + } +} diff --git a/tests/Engine/Check/MySQL/Column/ReservedKeywordsTest.php b/tests/Engine/Check/MySQL/Column/ReservedKeywordsTest.php new file mode 100644 index 0000000..c8bbe13 --- /dev/null +++ b/tests/Engine/Check/MySQL/Column/ReservedKeywordsTest.php @@ -0,0 +1,36 @@ +nonReservedKeywordColumn = $builder->name("nonReservedKeywordColumn")->generate(); + $this->nonReservedKeywordColumn->setTable($this->createTable()); + $this->reservedKeywordColumn = $builder->name(ReservedKeywords::RESERVED_KEYWORDS[array_rand(ReservedKeywords::RESERVED_KEYWORDS)])->generate(); + $this->reservedKeywordColumn->setTable($this->createTable()); + } + + public function testSupports() + { + $check = new ReservedKeywords(); + $this->assertTrue($check->supports($this->nonReservedKeywordColumn), "Ensure that the supports for $this->nonReservedKeywordColumn returns true."); + $this->assertTrue($check->supports($this->reservedKeywordColumn), "Ensure that the supports for $this->reservedKeywordColumn returns true."); + } + + public function testRun() + { + $check = new ReservedKeywords(); + $this->assertEquals(null, $check->run($this->nonReservedKeywordColumn), "Ensure that no report is returned for $this->nonReservedKeywordColumn."); + $this->assertEquals(Report::STATUS_CONCERN, $check->run($this->reservedKeywordColumn)->getStatus(), "Ensure that the report status for $this->reservedKeywordColumn is CONCERN."); + } +} diff --git a/tests/Engine/Check/MySQL/Column/SaneAutoIncrementTest.php b/tests/Engine/Check/MySQL/Column/SaneAutoIncrementTest.php new file mode 100644 index 0000000..51d759f --- /dev/null +++ b/tests/Engine/Check/MySQL/Column/SaneAutoIncrementTest.php @@ -0,0 +1,123 @@ +createTable(), + $this->createVirtualTable() + ]; + + $columns = [ + // Sane primary auto increment column + $builder->int(10)->unsigned()->primary()->auto_increment()->generate(), + // Unsigned primary auto increment column + $builder->int(10)->primary()->auto_increment()->generate(), + // Non-numeric primary auto increment column + $builder->varchar(30)->primary()->auto_increment()->generate(), + // Non-primary auto increment column + $builder->int(10)->unsigned()->auto_increment()->generate(), + // Primary generated (virtual) column + $builder->int(10)->primary()->generated()->generate(), + // Non auto-incrementing primary column + $builder->int(10)->unsigned()->primary()->generate(), + ]; + + $supports_results = [ + true, + true, + true, + true, + false, + false, + false, + false, + false, + false, + false, + false + ]; + + $run_results = [ + Report::STATUS_OK, + Report::STATUS_WARNING, + Report::STATUS_WARNING, + Report::STATUS_WARNING, + Report::STATUS_WARNING, + ]; + + $configurations = []; + $offset = 0; + + foreach ($tables as $table) { + foreach ($columns as $column) { + $tmp_table = clone $table; + + $tmp_column = clone $column; + $tmp_table->setColumns($tmp_column); + + $configurations[] = [ + $tmp_column, + $supports_results[$offset], + $run_results[$offset] ?? null + ]; + $offset++; + } + } + + return $configurations; + } + + /** + * Refactor this into normal tests.... + * + * @dataProvider providerColumnData + */ + public function testSupports($column, $supports, $status) + { + $check = new SaneAutoIncrement(); + $this->assertEquals($supports, $check->supports($column), "Ensure that the supports for $column returns $supports"); + } + + /** + * Refactor this into normal tests.... + * + * @dataProvider providerColumnData + */ + public function testRun($column, $supports, $status) + { + $check = new SaneAutoIncrement(); + // Only run checks for these tests + if ($supports) { + $report = $check->run($column); + $this->assertEquals($status, $report->getStatus(), "Ensure that the run for $column returns status $status"); + } else { + $this->assertTrue(true); + } + } + + public function testRunForCompoundKey() + { + $builder = new ColumnBuilder(); + $realSaneAutoIncrementColumn = $builder->int(10)->unsigned()->primary()->auto_increment()->generate(); + + $table = $this->createTable(); + $table->setColumns( + $realSaneAutoIncrementColumn, + $builder->int(10)->unsigned()->primary()->generate() + ); + + $check = new SaneAutoIncrement(); + $report = $check->run($realSaneAutoIncrementColumn); + $this->assertEquals(Report::STATUS_WARNING, $report->getStatus(), "We should not have a compound PRIMARY KEY where one of the columns is auto_incrementing."); + } +} diff --git a/tests/Engine/Check/MySQL/ColumnBuilder.php b/tests/Engine/Check/MySQL/ColumnBuilder.php new file mode 100644 index 0000000..b4f88c0 --- /dev/null +++ b/tests/Engine/Check/MySQL/ColumnBuilder.php @@ -0,0 +1,104 @@ + "MOCK_TABLE", + "COLUMN_NAME" => "MOCK_COLUMN", + "ORDINAL_POSITION" => "1", + "COLUMN_DEFAULT" => "0", + "IS_NULLABLE" => "NO", + "DATA_TYPE" => "int", + "CHARACTER_MAXIMUM_LENGTH" => NULL, + "CHARACTER_OCTET_LENGTH" => NULL, + "NUMERIC_PRECISION" => "10", + "NUMERIC_SCALE" => "0", + "DATETIME_PRECISION" => NULL, + "CHARACTER_SET_NAME" => NULL, + "COLLATION_NAME" => NULL, + "COLUMN_TYPE" => "int(20)", + "COLUMN_KEY" => "", + "EXTRA" => "", + "PRIVILEGES" => "select", + "COLUMN_COMMENT" => "", + "GENERATION_EXPRESSION" => "", + ]; + + private $override = []; + + public function name($name): ColumnBuilder + { + $this->override['COLUMN_NAME'] = $name; + return $this; + } + + public function character_encoding($encoding): ColumnBuilder + { + $this->override['CHARACTER_SET_NAME'] = $encoding; + return $this; + } + + public function nullable(): ColumnBuilder + { + $this->override['IS_NULLABLE'] = true; + return $this; + } + + public function primary(): ColumnBuilder + { + $this->override["COLUMN_KEY"] = "PRI"; + return $this; + } + + public function auto_increment(): ColumnBuilder + { + $this->override["EXTRA"] = "auto_increment"; + return $this; + } + + public function unsigned(): ColumnBuilder + { + $this->override['unsigned'] = true; + $this->override["COLUMN_TYPE"] = $this->override["DATA_TYPE"] . "(" . $this->override["NUMERIC_PRECISION"] . ") unsigned"; + return $this; + } + + public function int(int $precision = 10): ColumnBuilder + { + $this->override["DATA_TYPE"] = "int"; + $this->override["NUMERIC_PRECISION"] = $precision; + $this->override["COLUMN_TYPE"] = "int(" . $precision . ")" . (!empty($this->override['unsigned']) ? ' unsigned' : ''); + return $this; + } + + public function varchar(int $length = 10): ColumnBuilder + { + $this->override["DATA_TYPE"] = "varchar"; + $this->override["CHARACTER_MAXIMUM_LENGTH"] = $length; + $this->override["COLUMN_TYPE"] = "varchar($length)"; + return $this; + } + + public function generated(): ColumnBuilder + { + $this->override["GENERATION_EXPRESSION"] = "3.141592 * 42"; + return $this; + } + + public function generate(): Column + { + $column = Column::createFromInformationSchema(array_merge( + self::BASE, + $this->override + )); + $this->override = []; + + return $column; + } +} \ No newline at end of file diff --git a/tests/Engine/Check/MySQL/Table/AutoIncrementCapacityTest.php b/tests/Engine/Check/MySQL/Table/AutoIncrementCapacityTest.php new file mode 100644 index 0000000..a4d7c31 --- /dev/null +++ b/tests/Engine/Check/MySQL/Table/AutoIncrementCapacityTest.php @@ -0,0 +1,140 @@ +createTable(); + $emptyLargeCapacity->setSchemaAutoIncrementColumns(SchemaAutoIncrementColumns::createFromSys([ + 'table_schema' => 'tests', + 'table_name' => 'table_with_signed_autoincrement', + 'column_name' => 'id', + 'data_type' => 'int', + 'column_type' => 'int(11)', + 'is_signed' => 1, + 'is_unsigned' => 0, + 'max_value' => 2147483647, + 'auto_increment' => null, + 'auto_increment_ratio' => 0.0000, + ])); + + $midLargeCapacity = $this->createTable(); + $midLargeCapacity->setSchemaAutoIncrementColumns(SchemaAutoIncrementColumns::createFromSys([ + 'table_schema' => 'tests', + 'table_name' => 'table_with_signed_autoincrement', + 'column_name' => 'id', + 'data_type' => 'int', + 'column_type' => 'int(11)', + 'is_signed' => 1, + 'is_unsigned' => 0, + 'max_value' => 2147483647, + 'auto_increment' => 1331449861, + 'auto_increment_ratio' => 0.6200, + ])); + + $fullLargeCapacity = $this->createTable(); + $fullLargeCapacity->setSchemaAutoIncrementColumns(SchemaAutoIncrementColumns::createFromSys([ + 'table_schema' => 'tests', + 'table_name' => 'table_with_signed_autoincrement', + 'column_name' => 'id', + 'data_type' => 'int', + 'column_type' => 'int(11)', + 'is_signed' => 1, + 'is_unsigned' => 0, + 'max_value' => 2147483647, + 'auto_increment' => 1825361100, + 'auto_increment_ratio' => 0.8500, + ])); + + $emptySmallCapacity = $this->createTable(); + $emptySmallCapacity->setSchemaAutoIncrementColumns(SchemaAutoIncrementColumns::createFromSys([ + 'table_schema' => 'tests', + 'table_name' => 'table_with_signed_autoincrement', + 'column_name' => 'id', + 'data_type' => 'int', + 'column_type' => 'int(11)', + 'is_signed' => 1, + 'is_unsigned' => 0, + 'max_value' => 256, + 'auto_increment' => 0, + 'auto_increment_ratio' => 0.0000, + ])); + + $midSmallCapacity = $this->createTable(); + $midSmallCapacity->setSchemaAutoIncrementColumns(SchemaAutoIncrementColumns::createFromSys([ + 'table_schema' => 'tests', + 'table_name' => 'table_with_signed_autoincrement', + 'column_name' => 'id', + 'data_type' => 'int', + 'column_type' => 'int(11)', + 'is_signed' => 1, + 'is_unsigned' => 0, + 'max_value' => '256', + 'auto_increment' => 160, + 'auto_increment_ratio' => 0.6250, + ])); + + $fullSmallCapacity = $this->createTable(); + $fullSmallCapacity->setSchemaAutoIncrementColumns(SchemaAutoIncrementColumns::createFromSys([ + 'table_schema' => 'tests', + 'table_name' => 'table_with_signed_autoincrement', + 'column_name' => 'id', + 'data_type' => 'int', + 'column_type' => 'int(11)', + 'is_signed' => 1, + 'is_unsigned' => 0, + 'max_value' => '256', + 'auto_increment' => 210, + 'auto_increment_ratio' => 0.8203, + ])); + + return [ + [ $emptyLargeCapacity, Report::STATUS_OK ], + [ $midLargeCapacity, Report::STATUS_WARNING ], + [ $fullLargeCapacity, Report::STATUS_CRITICAL ], + [ $emptySmallCapacity, Report::STATUS_OK ], + [ $midSmallCapacity, Report::STATUS_WARNING ], + [ $fullSmallCapacity, Report::STATUS_WARNING ], + ]; + } + + /** + * @dataProvider providerTableData + */ + public function testSupports($table, $status) + { + $check = new AutoIncrementCapacity(); + $this->assertTrue($check->supports($table)); + } + + /** + * @dataProvider providerTableData + */ + public function testRun($table, $status) + { + $check = new AutoIncrementCapacity(); + $report = $check->run($table); + $this->assertEquals($status, $report->getStatus(), "Ensure that the run for $table returns status $status"); + + $data = $report->getData(); + if ($data) { + $this->assertEquals($table->schema_auto_increment_columns->max_value, $data['total']); + $this->assertEquals($table->schema_auto_increment_columns->auto_increment, $data['used']); + } + } + + public function testRunWithMissingSysData() + { + $this->expectException(MissingSysData::class); + + $check = new AutoIncrementCapacity(); + $check->run($this->createTable()); + } +} diff --git a/tests/Engine/Check/MySQL/Table/EmptyTableTest.php b/tests/Engine/Check/MySQL/Table/EmptyTableTest.php new file mode 100644 index 0000000..1833f3d --- /dev/null +++ b/tests/Engine/Check/MySQL/Table/EmptyTableTest.php @@ -0,0 +1,65 @@ +createEmptyTable(), + true + ], + [ + $this->createEmptyTable([ "DATA_FREE" => 110 ]), + true + ], + [ + $this->createTable(), + true + ], + [ + $this->createVirtualTable(), + false + ], + ]; + } + + public function providerTableDataForRun() { + return [ + [ + $this->createEmptyTable(), + Report::STATUS_WARNING + ], + [ + $this->createEmptyTable([ "DATA_FREE" => 110 ]), + Report::STATUS_INFO + ], + [ + $this->createTable(), + Report::STATUS_OK + ], + ]; + } + + /** + * @dataProvider providerTableDataForSupports + */ + public function testSupports($table, $result) + { + $check = new EmptyTable(); + $this->assertEquals($check->supports($table), $result, "Ensure that the supports for $table returns $result"); + } + + /** + * @dataProvider providerTableDataForRun + */ + public function testRun($table, $result) + { + $check = new EmptyTable(); + $this->assertEquals($check->run($table)->getStatus(), $result, "Ensure that the run for $table returns status $result"); + } +} diff --git a/tests/Engine/Check/MySQL/Table/MustHavePrimaryKeyTest.php b/tests/Engine/Check/MySQL/Table/MustHavePrimaryKeyTest.php new file mode 100644 index 0000000..d41daec --- /dev/null +++ b/tests/Engine/Check/MySQL/Table/MustHavePrimaryKeyTest.php @@ -0,0 +1,50 @@ +primaryKeyColumn = $builder->int(20) + ->unsigned() + ->primary() + ->auto_increment() + ->generate(); + + $this->nonPrimaryKeyColumn = $builder->int(20) + ->unsigned() + ->generate(); + } + + public function testSupports() + { + $check = new MustHavePrimaryKey(); + $this->assertTrue($check->supports($this->createTable()), "Ensure that tables are supported."); + $this->assertFalse($check->supports($this->createVirtualTable()), "Ensure that virtual tables are not supported."); + } + + public function testRun() + { + $check = new MustHavePrimaryKey(); + + $nonPrimaryTable = $this->createTable(); + $nonPrimaryTable->setColumns($this->nonPrimaryKeyColumn); + + $this->assertEquals(Report::STATUS_CRITICAL, $check->run($nonPrimaryTable)->getStatus(), "Ensure table without PRIMARY KEY is marked as CRITICAL."); + + $primaryTable = $this->createTable(); + $primaryTable->setColumns($this->primaryKeyColumn); + + $this->assertEquals(Report::STATUS_OK, $check->run($primaryTable)->getStatus(), "Ensure table without PRIMARY KEY is marked as OK."); + } +} diff --git a/tests/Engine/Check/MySQL/Table/RedundantIndexesTest.php b/tests/Engine/Check/MySQL/Table/RedundantIndexesTest.php new file mode 100644 index 0000000..4732174 --- /dev/null +++ b/tests/Engine/Check/MySQL/Table/RedundantIndexesTest.php @@ -0,0 +1,72 @@ +createTable(), + true + ], + [ + $this->createVirtualTable(), + false + ], + ]; + } + + public function providerTableDataForRun() { + $tableWithRedundantIndex = $this->createTable(); + $tableWithRedundantIndex->setSchemaRedundantIndexes( + SchemaRedundantIndexes::createFromSys([ + "table_schema" => "tests", + "table_name" => "table_with_unused_index", + "redundant_index_name" => "id_some", + "redundant_index_columns" => "id,some", + "redundant_index_non_unique" => 1, + "dominant_index_name" => "PRIMARY", + "dominant_index_columns" => "id", + "dominant_index_non_unique" => 0, + "subpart_exists" => 0, + "sql_drop_index" => "ALTER TABLE `tests`.`table_with_unused_index` DROP INDEX `id_some`", + ]) + ); + + return [ + [ + $tableWithRedundantIndex, + Report::STATUS_CONCERN + ], + [ + $this->createTable(), + Report::STATUS_OK + ], + ]; + } + + /** + * @dataProvider providerTableDataForSupports + */ + public function testSupports($table, $result) + { + $check = new RedundantIndexes(); + $this->assertEquals($check->supports($table), $result, "Ensure that the supports for $table returns $result"); + } + + /** + * @dataProvider providerTableDataForRun + */ + public function testRun($table, $result) + { + $check = new RedundantIndexes(); + $this->assertEquals($check->run($table)->getStatus(), $result, "Ensure that the run for $table returns status $result"); + } +} diff --git a/tests/Engine/Check/MySQL/Table/SaneInnoDbPrimaryKeyTest.php b/tests/Engine/Check/MySQL/Table/SaneInnoDbPrimaryKeyTest.php new file mode 100644 index 0000000..ba8dbdb --- /dev/null +++ b/tests/Engine/Check/MySQL/Table/SaneInnoDbPrimaryKeyTest.php @@ -0,0 +1,103 @@ +simplePrimaryKeyColumn = $builder->int(20) + ->unsigned() + ->primary() + ->auto_increment() + ->generate(); + + $this->mediumPrimaryKeyColumn = $builder->varchar(8) + ->primary() + ->generate(); + + $this->complexPrimaryKeyColumn = $builder->varchar(16) + ->primary() + ->generate(); + + $this->intColumn= $builder->int(10) + ->generate(); + + $this->stringColumn= $builder->varchar(16) + ->generate(); + } + + public function testSupports() + { + $check = new SaneInnoDbPrimaryKey(); + $this->assertTrue($check->supports($this->createTable()), "Ensure that InnoDb tables are supported."); + $this->assertFalse($check->supports($this->createTable([ "ENGINE" => "MyISAM" ])), "Ensure that non-InnoDB tables are not supported."); + $this->assertFalse($check->supports($this->createVirtualTable()), "Ensure that virtual tables are not supported."); + } + + public function testRun() + { + $check = new SaneInnoDbPrimaryKey(); + + $this->assertNull($check->run($this->createTable()), "Ensure table without a PRIMARY KEY is ignored."); + + $tableWithNoOtherIndex = $this->createTable(); + $tableWithNoOtherIndex->setColumns(clone $this->simplePrimaryKeyColumn); + $this->assertEquals(Report::STATUS_OK, $check->run($tableWithNoOtherIndex)->getStatus(), "Ensure table without other indexes is fine."); + + $smallIndexColumn = clone $this->intColumn; + $tableWithSmallIndex = $this->createTable(); + $tableWithSmallIndex->setColumns(clone $this->simplePrimaryKeyColumn, $smallIndexColumn); + $index = new Index('simple'); + $index->setColumns($smallIndexColumn); + $tableWithSmallIndex->setIndexes($index); + $this->assertEquals(Report::STATUS_OK, $check->run($tableWithSmallIndex)->getStatus(), "Ensure table with a small PRIMARY KEY and a small indexes is fine."); + + $largeIndexColumn = clone $this->stringColumn; + $tableWithLargeIndex = $this->createTable(); + $tableWithLargeIndex->setColumns(clone $this->simplePrimaryKeyColumn, $largeIndexColumn); + $index = new Index('large'); + $index->setColumns($largeIndexColumn); + $tableWithLargeIndex->setIndexes($index); + $this->assertEquals(Report::STATUS_OK, $check->run($tableWithLargeIndex)->getStatus(), "Ensure table with a small PRIMARY KEY and a large indexes is fine."); + + $smallIndexColumn = clone $this->intColumn; + $tableWithMediumPrimaryAndSmallIndex = $this->createTable(); + $tableWithMediumPrimaryAndSmallIndex->setColumns(clone $this->mediumPrimaryKeyColumn, $smallIndexColumn); + $index = new Index('simple'); + $index->setColumns($smallIndexColumn); + $tableWithMediumPrimaryAndSmallIndex->setIndexes($index); + $this->assertEquals(Report::STATUS_OK, $check->run($tableWithMediumPrimaryAndSmallIndex)->getStatus(), "Ensure table with a medium PRIMARY KEY and a small indexes is a warning."); + + $smallIndexColumn = clone $this->intColumn; + $tableWithLargePrimaryAndSmallIndex = $this->createTable(); + $tableWithLargePrimaryAndSmallIndex->setColumns(clone $this->complexPrimaryKeyColumn, $smallIndexColumn); + $index = new Index('simple'); + $index->setColumns($smallIndexColumn); + $tableWithLargePrimaryAndSmallIndex->setIndexes($index); + $this->assertEquals(Report::STATUS_WARNING, $check->run($tableWithLargePrimaryAndSmallIndex)->getStatus(), "Ensure table with a large PRIMARY KEY and a small indexes is a warning."); + + $largeIndexColumn = clone $this->stringColumn; + $tableWithLargePrimaryAndLargeIndex = $this->createTable(); + $tableWithLargePrimaryAndLargeIndex->setColumns(clone $this->complexPrimaryKeyColumn, $largeIndexColumn); + $index = new Index('large'); + $index->setColumns($largeIndexColumn); + $tableWithLargePrimaryAndLargeIndex->setIndexes($index); + $this->assertEquals(Report::STATUS_WARNING, $check->run($tableWithLargePrimaryAndLargeIndex)->getStatus(), "Ensure table with a large PRIMARY KEY and a large indexes is a warning."); + } +} diff --git a/tests/Engine/Entity/IndexTest.php b/tests/Engine/Entity/IndexTest.php new file mode 100644 index 0000000..9ac9460 --- /dev/null +++ b/tests/Engine/Entity/IndexTest.php @@ -0,0 +1,56 @@ +index = new Index("MOCK_INDEX"); + $this->index->setTable(Table::createFromInformationSchema([ + "TABLE_CATALOG" => "MOCK_CATALOG", + "TABLE_SCHEMA" => "MOCK_SCHEMA", + "TABLE_NAME" => "MOCK_TABLE", + "TABLE_TYPE" => "BASE TABLE", + "ENGINE" => "InnoDB", + "VERSION" => "10", + "ROW_FORMAT" => "Fixed", + "TABLE_ROWS" => 200, + "AVG_ROW_LENGTH" => 384, + "DATA_LENGTH" => 2311, + "MAX_DATA_LENGTH" => 16434816, + "INDEX_LENGTH" => 0, + "DATA_FREE" => 0, + "AUTO_INCREMENT" => null, + "CREATE_TIME" => "2020-05-30 11:29:56", + "UPDATE_TIME" => null, + "CHECK_TIME" => null, + "TABLE_COLLATION" => "utf8_general_ci", + "CHECKSUM" => null, + "CREATE_OPTIONS" => "", + "TABLE_COMMENT" => "", + ])); + } + + public function testIsVirtual() + { + $this->assertFalse($this->index->isVirtual()); + } + + public function testGetName() + { + $this->assertEquals("MOCK_INDEX", $this->index->getName()); + } + + public function test__toString() + { + $this->assertEquals("MOCK_TABLE.MOCK_INDEX", (string)$this->index); + } +} diff --git a/tests/Engine/Entity/MySQL/ColumnTest.php b/tests/Engine/Entity/MySQL/ColumnTest.php new file mode 100644 index 0000000..07a249f --- /dev/null +++ b/tests/Engine/Entity/MySQL/ColumnTest.php @@ -0,0 +1,170 @@ +table = Table::createFromInformationSchema([ + "TABLE_CATALOG" => "def", + "TABLE_SCHEMA" => "information_schema", + "TABLE_NAME" => "CHARACTER_SETS", + "TABLE_TYPE" => "SYSTEM VIEW", + "ENGINE" => "MEMORY", + "VERSION" => "10", + "ROW_FORMAT" => "Fixed", + "TABLE_ROWS" => null, + "AVG_ROW_LENGTH" => 384, + "DATA_LENGTH" => 0, + "MAX_DATA_LENGTH" => 16434816, + "INDEX_LENGTH" => 0, + "DATA_FREE" => 0, + "AUTO_INCREMENT" => null, + "CREATE_TIME" => "2020-05-30 11:29:56", + "UPDATE_TIME" => null, + "CHECK_TIME" => null, + "TABLE_COLLATION" => "utf8_general_ci", + "CHECKSUM" => null, + "CREATE_OPTIONS" => "max_rows=43690", + "TABLE_COMMENT" => "", + ]); + + $this->integerColumn = Column::createFromInformationSchema([ + "TABLE_NAME" => "CHARACTER_SETS", + "COLUMN_NAME" => "id", + "ORDINAL_POSITION" => 1, + "COLUMN_DEFAULT" => "0", + "IS_NULLABLE" => "NO", + "DATA_TYPE" => "int", + "CHARACTER_MAXIMUM_LENGTH" => NULL, + "CHARACTER_OCTET_LENGTH" => NULL, + "NUMERIC_PRECISION" => 10, + "NUMERIC_SCALE" => 0, + "DATETIME_PRECISION" => NULL, + "CHARACTER_SET_NAME" => NULL, + "COLLATION_NAME" => NULL, + "COLUMN_TYPE" => "int(20) unsigned", + "COLUMN_KEY" => "PRI", + "EXTRA" => "auto_increment", + "PRIVILEGES" => "select", + "COLUMN_COMMENT" => "", + "GENERATION_EXPRESSION" => "", + ]); + + $this->stringColumn = Column::createFromInformationSchema([ + "TABLE_NAME" => "CHARACTER_SETS", + "COLUMN_NAME" => "name", + "ORDINAL_POSITION" => 2, + "COLUMN_DEFAULT" => "", + "IS_NULLABLE" => "NO", + "DATA_TYPE" => "varchar", + "CHARACTER_MAXIMUM_LENGTH" => 30, + "CHARACTER_OCTET_LENGTH" => 96, + "NUMERIC_PRECISION" => NULL, + "NUMERIC_SCALE" => NULL, + "DATETIME_PRECISION" => NULL, + "CHARACTER_SET_NAME" => "utf8", + "COLLATION_NAME" => "utf8_general_ci", + "COLUMN_TYPE" => "varchar(30)", + "COLUMN_KEY" => "", + "EXTRA" => "", + "PRIVILEGES" => "select", + "COLUMN_COMMENT" => "", + "GENERATION_EXPRESSION" => "derived", + ]); + + $this->invalidColumn = Column::createFromInformationSchema([ + "TABLE_NAME" => "CHARACTER_SETS", + "COLUMN_NAME" => "invalid", + "ORDINAL_POSITION" => 3, + "COLUMN_DEFAULT" => "", + "IS_NULLABLE" => "NO", + "DATA_TYPE" => "invalid_type", + "CHARACTER_MAXIMUM_LENGTH" => NULL, + "CHARACTER_OCTET_LENGTH" => NULL, + "NUMERIC_PRECISION" => NULL, + "NUMERIC_SCALE" => NULL, + "DATETIME_PRECISION" => NULL, + "CHARACTER_SET_NAME" => NULL, + "COLLATION_NAME" => NULL, + "COLUMN_TYPE" => "invalid_type", + "COLUMN_KEY" => "", + "EXTRA" => "", + "PRIVILEGES" => "", + "COLUMN_COMMENT" => "", + "GENERATION_EXPRESSION" => "", + ]); + + $this->table->setColumns($this->integerColumn, $this->stringColumn); + } + + public function test__getName() + { + $this->assertEquals($this->integerColumn->getName(), "id", "Verify that the column name is returned from getName()."); + $this->assertEquals($this->stringColumn->getName(), "name", "Verify that the column name is returned from getName()."); + } + + public function test__toString() + { + $this->assertEquals((string)$this->integerColumn, "CHARACTER_SETS.id", "Verify that the schema, table and column name is returned from __toString()."); + $this->assertEquals((string)$this->stringColumn, "CHARACTER_SETS.name", "Verify that the schema, table and column name is returned from __toString()."); + } + + public function test__isVirtual() + { + $this->assertFalse($this->integerColumn->isVirtual(), "Verify that the column is correctly identified as not virtual."); + $this->assertTrue($this->stringColumn->isVirtual(), "Verify that the column is correctly identified as virtual."); + } + + public function test__isPartOfPrimaryKey() + { + $this->assertTrue($this->integerColumn->isPartOfPrimaryKey(), "Verify if the column is part of a PRIMARY KEY or not."); + $this->assertFalse($this->stringColumn->isPartOfPrimaryKey(), "Verify if the column is part of a PRIMARY KEY or not."); + } + + public function test__isSigned() + { + $this->assertFalse($this->integerColumn->isSigned(), "Verify if the column is signed or not."); + $this->assertFalse($this->stringColumn->isSigned(), "Verify if the column is signed or not."); + } + + public function test__isAutoIncrementing() + { + $this->assertTrue($this->integerColumn->isAutoIncrementing(), "Verify if the column is auto-incrementing or not."); + $this->assertFalse($this->stringColumn->isAutoIncrementing(), "Verify if the column is auto-incrementing or not."); + } + + public function test__isInteger() + { + $this->assertTrue($this->integerColumn->isInteger(), "Verify if the column is an integer type or not."); + $this->assertFalse($this->stringColumn->isInteger(), "Verify if the column is an integer type or not."); + } + + public function test__isNumeric() + { + $this->assertTrue($this->integerColumn->isNumeric(), "Verify if the column is a number type or not."); + $this->assertFalse($this->stringColumn->isNumeric(), "Verify if the column is a number type or not."); + } + + public function test__getStorageByteSize() + { + $this->assertEquals(4, $this->integerColumn->getStorageByteSize(), "Return the storage size in bytes for the field."); + $this->assertEquals(31, $this->stringColumn->getStorageByteSize(), "Return the storage size in bytes for the field."); + } + + public function test__invalidColumnCalling__getStorageByteSize() + { + $this->expectException(UnknownColumnType::class); + $this->invalidColumn->getStorageByteSize(); + } +} diff --git a/tests/Engine/Entity/MySQL/TableTest.php b/tests/Engine/Entity/MySQL/TableTest.php new file mode 100644 index 0000000..126554c --- /dev/null +++ b/tests/Engine/Entity/MySQL/TableTest.php @@ -0,0 +1,62 @@ +table = Table::createFromInformationSchema([ + "TABLE_CATALOG" => "def", + "TABLE_SCHEMA" => "information_schema", + "TABLE_NAME" => "CHARACTER_SETS", + "TABLE_TYPE" => "SYSTEM VIEW", + "ENGINE" => "MEMORY", + "VERSION" => "10", + "ROW_FORMAT" => "Fixed", + "TABLE_ROWS" => null, + "AVG_ROW_LENGTH" => 384, + "DATA_LENGTH" => 0, + "MAX_DATA_LENGTH" => 16434816, + "INDEX_LENGTH" => 0, + "DATA_FREE" => 0, + "AUTO_INCREMENT" => null, + "CREATE_TIME" => "2020-05-30 11:29:56", + "UPDATE_TIME" => null, + "CHECK_TIME" => null, + "TABLE_COLLATION" => "utf8_general_ci", + "CHECKSUM" => null, + "CREATE_OPTIONS" => "max_rows=43690", + "TABLE_COMMENT" => "", + ]); + } + + public function test__getName() + { + $this->assertEquals($this->table->getName(), "CHARACTER_SETS", "Verify that the table name is returned from getName()."); + } + + public function test__toString() + { + $this->assertEquals((string)$this->table, "CHARACTER_SETS", "Verify that the schema and table name is returned from __toString()."); + } + + public function test__isVirtual() + { + $this->assertTrue($this->table->isVirtual(), "Verify that the table is correctly identified as virtual."); + + // Verify that an shell table is considered virtual by default + $table = new Table("MOCK_SCHEMA", "MOCK_TABLE"); + $this->assertTrue($table->isVirtual()); + } + +} diff --git a/tests/Engine/ReportTest.php b/tests/Engine/ReportTest.php new file mode 100644 index 0000000..8e2a604 --- /dev/null +++ b/tests/Engine/ReportTest.php @@ -0,0 +1,109 @@ + "MOCK_CATALOG", + "TABLE_SCHEMA" => "MOCK_SCHEMA", + "TABLE_NAME" => "MOCK_TABLE", + "TABLE_TYPE" => "BASE TABLE", + "ENGINE" => "InnoDB", + "VERSION" => "10", + "ROW_FORMAT" => "Fixed", + "TABLE_ROWS" => 200, + "AVG_ROW_LENGTH" => 384, + "DATA_LENGTH" => 2311, + "MAX_DATA_LENGTH" => 16434816, + "INDEX_LENGTH" => 0, + "DATA_FREE" => 0, + "AUTO_INCREMENT" => null, + "CREATE_TIME" => "2020-05-30 11:29:56", + "UPDATE_TIME" => null, + "CHECK_TIME" => null, + "TABLE_COLLATION" => "utf8_general_ci", + "CHECKSUM" => null, + "CREATE_OPTIONS" => "", + "TABLE_COMMENT" => "", + ]; + + $this->entity = Table::createFromInformationSchema($base); + $this->check = new EmptyTable(); + $this->report = new Report( + $this->check, + $this->entity, + Report::STATUS_OK, + [ "Message 1", "Message 2" ], + [ "data1" => "a", "data2" => [ 1, 2] ] + ); + } + + public function testGetStatus() + { + $this->assertEquals(Report::STATUS_OK, $this->report->getStatus()); + } + + public function testGetStatusLabel() + { + $this->assertEquals("Ok", $this->report->getStatusLabel()); + } + + public function testIsValidStatus() + { + $this->assertTrue(Report::isValidStatus(Report::STATUS_OK)); + $this->assertTrue(Report::isValidStatus(Report::STATUS_INFO)); + $this->assertTrue(Report::isValidStatus(Report::STATUS_CONCERN)); + $this->assertTrue(Report::isValidStatus(Report::STATUS_WARNING)); + $this->assertTrue(Report::isValidStatus(Report::STATUS_CRITICAL)); + $this->assertFalse(Report::isValidStatus(100)); + } + + public function testGetMessages() + { + $this->assertEquals([ "Message 1", "Message 2" ], $this->report->getMessages()); + } + + public function testGetData() + { + $this->assertEquals([ "data1" => "a", "data2" => [ 1, 2] ], $this->report->getData()); + } + + public function testGetCheckLabel() + { + $this->assertEquals("EmptyTable", $this->report->getCheckLabel()); + } + + public function testGetEntity() + { + $this->assertEquals($this->entity, $this->report->getEntity()); + } + + public function testGetCheck() + { + $this->assertEquals($this->check, $this->report->getCheck()); + } + + public function test__construct_invalid_status() + { + $this->expectException("\Cadfael\Engine\Exception\InvalidStatus"); + new Report($this->check, $this->entity, 100); + } +}