api de gestion de ticket, basé sur php-crud-api. Le but est de décorrélé les outils de gestion des données, afin
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

api.php 245KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570457145724573457445754576457745784579458045814582458345844585458645874588458945904591459245934594459545964597459845994600460146024603460446054606460746084609461046114612461346144615461646174618461946204621462246234624462546264627462846294630463146324633463446354636463746384639464046414642464346444645464646474648464946504651465246534654465546564657465846594660466146624663466446654666466746684669467046714672467346744675467646774678467946804681468246834684468546864687468846894690469146924693469446954696469746984699470047014702470347044705470647074708470947104711471247134714471547164717471847194720472147224723472447254726472747284729473047314732473347344735473647374738473947404741474247434744474547464747474847494750475147524753475447554756475747584759476047614762476347644765476647674768476947704771477247734774477547764777477847794780478147824783478447854786478747884789479047914792479347944795479647974798479948004801480248034804480548064807480848094810481148124813481448154816481748184819482048214822482348244825482648274828482948304831483248334834483548364837483848394840484148424843484448454846484748484849485048514852485348544855485648574858485948604861486248634864486548664867486848694870487148724873487448754876487748784879488048814882488348844885488648874888488948904891489248934894489548964897489848994900490149024903490449054906490749084909491049114912491349144915491649174918491949204921492249234924492549264927492849294930493149324933493449354936493749384939494049414942494349444945494649474948494949504951495249534954495549564957495849594960496149624963496449654966496749684969497049714972497349744975497649774978497949804981498249834984498549864987498849894990499149924993499449954996499749984999500050015002500350045005500650075008500950105011501250135014501550165017501850195020502150225023502450255026502750285029503050315032503350345035503650375038503950405041504250435044504550465047504850495050505150525053505450555056505750585059506050615062506350645065506650675068506950705071507250735074507550765077507850795080508150825083508450855086508750885089509050915092509350945095509650975098509951005101510251035104510551065107510851095110511151125113511451155116511751185119512051215122512351245125512651275128512951305131513251335134513551365137513851395140514151425143514451455146514751485149515051515152515351545155515651575158515951605161516251635164516551665167516851695170517151725173517451755176517751785179518051815182518351845185518651875188518951905191519251935194519551965197519851995200520152025203520452055206520752085209521052115212521352145215521652175218521952205221522252235224522552265227522852295230523152325233523452355236523752385239524052415242524352445245524652475248524952505251525252535254525552565257525852595260526152625263526452655266526752685269527052715272527352745275527652775278527952805281528252835284528552865287528852895290529152925293529452955296529752985299530053015302530353045305530653075308530953105311531253135314531553165317531853195320532153225323532453255326532753285329533053315332533353345335533653375338533953405341534253435344534553465347534853495350535153525353535453555356535753585359536053615362536353645365536653675368536953705371537253735374537553765377537853795380538153825383538453855386538753885389539053915392539353945395539653975398539954005401540254035404540554065407540854095410541154125413541454155416541754185419542054215422542354245425542654275428542954305431543254335434543554365437543854395440544154425443544454455446544754485449545054515452545354545455545654575458545954605461546254635464546554665467546854695470547154725473547454755476547754785479548054815482548354845485548654875488548954905491549254935494549554965497549854995500550155025503550455055506550755085509551055115512551355145515551655175518551955205521552255235524552555265527552855295530553155325533553455355536553755385539554055415542554355445545554655475548554955505551555255535554555555565557555855595560556155625563556455655566556755685569557055715572557355745575557655775578557955805581558255835584558555865587558855895590559155925593559455955596559755985599560056015602560356045605560656075608560956105611561256135614561556165617561856195620562156225623562456255626562756285629563056315632563356345635563656375638563956405641564256435644564556465647564856495650565156525653565456555656565756585659566056615662566356645665566656675668566956705671567256735674567556765677567856795680568156825683568456855686568756885689569056915692569356945695569656975698569957005701570257035704570557065707570857095710571157125713571457155716571757185719572057215722572357245725572657275728572957305731573257335734573557365737573857395740574157425743574457455746574757485749575057515752575357545755575657575758575957605761576257635764576557665767576857695770577157725773577457755776577757785779578057815782578357845785578657875788578957905791579257935794579557965797579857995800580158025803580458055806580758085809581058115812581358145815581658175818581958205821582258235824582558265827582858295830583158325833583458355836583758385839584058415842584358445845584658475848584958505851585258535854585558565857585858595860586158625863586458655866586758685869587058715872587358745875587658775878587958805881588258835884588558865887588858895890589158925893589458955896589758985899590059015902590359045905590659075908590959105911591259135914591559165917591859195920592159225923592459255926592759285929593059315932593359345935593659375938593959405941594259435944594559465947594859495950595159525953595459555956595759585959596059615962596359645965596659675968596959705971597259735974597559765977597859795980598159825983598459855986598759885989599059915992599359945995599659975998599960006001600260036004600560066007600860096010601160126013601460156016601760186019602060216022602360246025602660276028602960306031603260336034603560366037603860396040604160426043604460456046604760486049605060516052605360546055605660576058605960606061606260636064606560666067606860696070607160726073607460756076607760786079608060816082608360846085608660876088608960906091609260936094609560966097609860996100610161026103610461056106610761086109611061116112611361146115611661176118611961206121612261236124612561266127612861296130613161326133613461356136613761386139614061416142614361446145614661476148614961506151615261536154615561566157615861596160616161626163616461656166616761686169617061716172617361746175617661776178617961806181618261836184618561866187618861896190619161926193619461956196619761986199620062016202620362046205620662076208620962106211621262136214621562166217621862196220622162226223622462256226622762286229623062316232623362346235623662376238623962406241624262436244624562466247624862496250625162526253625462556256625762586259626062616262626362646265626662676268626962706271627262736274627562766277627862796280628162826283628462856286628762886289629062916292629362946295629662976298629963006301630263036304630563066307630863096310631163126313631463156316631763186319632063216322632363246325632663276328632963306331633263336334633563366337633863396340634163426343634463456346634763486349635063516352635363546355635663576358635963606361636263636364636563666367636863696370637163726373637463756376637763786379638063816382638363846385638663876388638963906391639263936394639563966397639863996400640164026403640464056406640764086409641064116412641364146415641664176418641964206421642264236424642564266427642864296430643164326433643464356436643764386439644064416442644364446445644664476448644964506451645264536454645564566457645864596460646164626463646464656466646764686469647064716472647364746475647664776478647964806481648264836484648564866487648864896490649164926493649464956496649764986499650065016502650365046505650665076508650965106511651265136514651565166517651865196520652165226523652465256526652765286529653065316532653365346535653665376538653965406541654265436544654565466547654865496550655165526553655465556556655765586559656065616562656365646565656665676568656965706571657265736574657565766577657865796580658165826583658465856586658765886589659065916592659365946595659665976598659966006601660266036604660566066607660866096610661166126613661466156616661766186619662066216622662366246625662666276628662966306631663266336634663566366637663866396640664166426643664466456646664766486649665066516652665366546655665666576658665966606661666266636664666566666667666866696670667166726673667466756676667766786679668066816682668366846685668666876688668966906691669266936694669566966697669866996700670167026703670467056706670767086709671067116712671367146715671667176718671967206721672267236724672567266727672867296730673167326733673467356736673767386739674067416742674367446745674667476748674967506751675267536754675567566757675867596760676167626763676467656766676767686769677067716772677367746775677667776778677967806781678267836784678567866787678867896790679167926793679467956796679767986799680068016802680368046805680668076808680968106811681268136814681568166817681868196820682168226823682468256826682768286829683068316832683368346835683668376838683968406841684268436844684568466847684868496850685168526853685468556856685768586859686068616862686368646865686668676868686968706871687268736874687568766877687868796880688168826883688468856886688768886889689068916892689368946895689668976898689969006901690269036904690569066907690869096910691169126913691469156916691769186919692069216922692369246925692669276928692969306931693269336934693569366937693869396940694169426943694469456946694769486949695069516952695369546955695669576958695969606961696269636964696569666967696869696970697169726973697469756976697769786979698069816982698369846985698669876988698969906991699269936994699569966997699869997000700170027003700470057006700770087009701070117012701370147015701670177018701970207021702270237024702570267027702870297030703170327033703470357036703770387039704070417042704370447045704670477048704970507051705270537054705570567057705870597060706170627063706470657066706770687069707070717072707370747075707670777078707970807081708270837084708570867087708870897090709170927093709470957096709770987099710071017102710371047105710671077108710971107111711271137114711571167117711871197120712171227123712471257126712771287129713071317132713371347135713671377138713971407141714271437144714571467147714871497150715171527153715471557156715771587159716071617162716371647165716671677168716971707171717271737174717571767177717871797180718171827183718471857186718771887189719071917192719371947195719671977198719972007201720272037204720572067207720872097210721172127213721472157216721772187219722072217222722372247225722672277228722972307231723272337234723572367237723872397240724172427243724472457246724772487249725072517252725372547255725672577258725972607261726272637264726572667267726872697270727172727273727472757276727772787279728072817282728372847285728672877288728972907291729272937294729572967297729872997300730173027303730473057306730773087309731073117312731373147315731673177318731973207321732273237324732573267327732873297330733173327333733473357336733773387339734073417342734373447345734673477348734973507351735273537354735573567357735873597360736173627363736473657366736773687369737073717372737373747375737673777378737973807381738273837384738573867387738873897390739173927393739473957396739773987399740074017402740374047405740674077408740974107411741274137414741574167417741874197420742174227423742474257426742774287429743074317432743374347435743674377438743974407441744274437444744574467447744874497450745174527453745474557456745774587459746074617462746374647465746674677468
  1. <?php
  2. /**
  3. * PHP-CRUD-API v2 License: MIT
  4. * Maurits van der Schee: maurits@vdschee.nl
  5. * https://github.com/mevdschee/php-crud-api
  6. *
  7. * Dependencies:
  8. * - vendor/psr/*: PHP-FIG
  9. * https://github.com/php-fig
  10. * - vendor/nyholm/*: Tobias Nyholm
  11. * https://github.com/Nyholm
  12. **/
  13. namespace Tqdev\PhpCrudApi;
  14. // file: vendor/psr/http-factory/src/RequestFactoryInterface.php
  15. interface RequestFactoryInterface
  16. {
  17. public function createRequest(string $method, $uri): RequestInterface;
  18. }
  19. // file: vendor/psr/http-factory/src/ResponseFactoryInterface.php
  20. interface ResponseFactoryInterface
  21. {
  22. public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface;
  23. }
  24. // file: vendor/psr/http-factory/src/ServerRequestFactoryInterface.php
  25. interface ServerRequestFactoryInterface
  26. {
  27. public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface;
  28. }
  29. // file: vendor/psr/http-factory/src/StreamFactoryInterface.php
  30. interface StreamFactoryInterface
  31. {
  32. public function createStream(string $content = ''): StreamInterface;
  33. public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface;
  34. public function createStreamFromResource($resource): StreamInterface;
  35. }
  36. // file: vendor/psr/http-factory/src/UploadedFileFactoryInterface.php
  37. interface UploadedFileFactoryInterface
  38. {
  39. public function createUploadedFile(
  40. StreamInterface $stream,
  41. int $size = null,
  42. int $error = \UPLOAD_ERR_OK,
  43. string $clientFilename = null,
  44. string $clientMediaType = null
  45. ): UploadedFileInterface;
  46. }
  47. // file: vendor/psr/http-factory/src/UriFactoryInterface.php
  48. interface UriFactoryInterface
  49. {
  50. public function createUri(string $uri = ''): UriInterface;
  51. }
  52. // file: vendor/psr/http-message/src/MessageInterface.php
  53. interface MessageInterface
  54. {
  55. public function getProtocolVersion();
  56. public function withProtocolVersion($version);
  57. public function getHeaders();
  58. public function hasHeader($name);
  59. public function getHeader($name);
  60. public function getHeaderLine($name);
  61. public function withHeader($name, $value);
  62. public function withAddedHeader($name, $value);
  63. public function withoutHeader($name);
  64. public function getBody();
  65. public function withBody(StreamInterface $body);
  66. }
  67. // file: vendor/psr/http-message/src/RequestInterface.php
  68. interface RequestInterface extends MessageInterface
  69. {
  70. public function getRequestTarget();
  71. public function withRequestTarget($requestTarget);
  72. public function getMethod();
  73. public function withMethod($method);
  74. public function getUri();
  75. public function withUri(UriInterface $uri, $preserveHost = false);
  76. }
  77. // file: vendor/psr/http-message/src/ResponseInterface.php
  78. interface ResponseInterface extends MessageInterface
  79. {
  80. public function getStatusCode();
  81. public function withStatus($code, $reasonPhrase = '');
  82. public function getReasonPhrase();
  83. }
  84. // file: vendor/psr/http-message/src/ServerRequestInterface.php
  85. interface ServerRequestInterface extends RequestInterface
  86. {
  87. public function getServerParams();
  88. public function getCookieParams();
  89. public function withCookieParams(array $cookies);
  90. public function getQueryParams();
  91. public function withQueryParams(array $query);
  92. public function getUploadedFiles();
  93. public function withUploadedFiles(array $uploadedFiles);
  94. public function getParsedBody();
  95. public function withParsedBody($data);
  96. public function getAttributes();
  97. public function getAttribute($name, $default = null);
  98. public function withAttribute($name, $value);
  99. public function withoutAttribute($name);
  100. }
  101. // file: vendor/psr/http-message/src/StreamInterface.php
  102. interface StreamInterface
  103. {
  104. public function __toString();
  105. public function close();
  106. public function detach();
  107. public function getSize();
  108. public function tell();
  109. public function eof();
  110. public function isSeekable();
  111. public function seek($offset, $whence = SEEK_SET);
  112. public function rewind();
  113. public function isWritable();
  114. public function write($string);
  115. public function isReadable();
  116. public function read($length);
  117. public function getContents();
  118. public function getMetadata($key = null);
  119. }
  120. // file: vendor/psr/http-message/src/UploadedFileInterface.php
  121. interface UploadedFileInterface
  122. {
  123. public function getStream();
  124. public function moveTo($targetPath);
  125. public function getSize();
  126. public function getError();
  127. public function getClientFilename();
  128. public function getClientMediaType();
  129. }
  130. // file: vendor/psr/http-message/src/UriInterface.php
  131. interface UriInterface
  132. {
  133. public function getScheme();
  134. public function getAuthority();
  135. public function getUserInfo();
  136. public function getHost();
  137. public function getPort();
  138. public function getPath();
  139. public function getQuery();
  140. public function getFragment();
  141. public function withScheme($scheme);
  142. public function withUserInfo($user, $password = null);
  143. public function withHost($host);
  144. public function withPort($port);
  145. public function withPath($path);
  146. public function withQuery($query);
  147. public function withFragment($fragment);
  148. public function __toString();
  149. }
  150. // file: vendor/psr/http-server-handler/src/RequestHandlerInterface.php
  151. interface RequestHandlerInterface
  152. {
  153. public function handle(ServerRequestInterface $request): ResponseInterface;
  154. }
  155. // file: vendor/psr/http-server-middleware/src/MiddlewareInterface.php
  156. interface MiddlewareInterface
  157. {
  158. public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface;
  159. }
  160. // file: vendor/nyholm/psr7/src/Factory/Psr17Factory.php
  161. final class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
  162. {
  163. public function createRequest(string $method, $uri): RequestInterface
  164. {
  165. return new Request($method, $uri);
  166. }
  167. public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
  168. {
  169. return new Response($code, [], null, '1.1', $reasonPhrase);
  170. }
  171. public function createStream(string $content = ''): StreamInterface
  172. {
  173. return Stream::create($content);
  174. }
  175. public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
  176. {
  177. $resource = @\fopen($filename, $mode);
  178. if (false === $resource) {
  179. if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'])) {
  180. throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.');
  181. }
  182. throw new \RuntimeException('The file ' . $filename . ' cannot be opened.');
  183. }
  184. return Stream::create($resource);
  185. }
  186. public function createStreamFromResource($resource): StreamInterface
  187. {
  188. return Stream::create($resource);
  189. }
  190. public function createUploadedFile(StreamInterface $stream, int $size = null, int $error = \UPLOAD_ERR_OK, string $clientFilename = null, string $clientMediaType = null): UploadedFileInterface
  191. {
  192. if (null === $size) {
  193. $size = $stream->getSize();
  194. }
  195. return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
  196. }
  197. public function createUri(string $uri = ''): UriInterface
  198. {
  199. return new Uri($uri);
  200. }
  201. public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
  202. {
  203. return new ServerRequest($method, $uri, [], null, '1.1', $serverParams);
  204. }
  205. }
  206. // file: vendor/nyholm/psr7/src/MessageTrait.php
  207. trait MessageTrait
  208. {
  209. private $headers = [];
  210. private $headerNames = [];
  211. private $protocol = '1.1';
  212. private $stream;
  213. public function getProtocolVersion(): string
  214. {
  215. return $this->protocol;
  216. }
  217. public function withProtocolVersion($version): self
  218. {
  219. if ($this->protocol === $version) {
  220. return $this;
  221. }
  222. $new = clone $this;
  223. $new->protocol = $version;
  224. return $new;
  225. }
  226. public function getHeaders(): array
  227. {
  228. return $this->headers;
  229. }
  230. public function hasHeader($header): bool
  231. {
  232. return isset($this->headerNames[\strtolower($header)]);
  233. }
  234. public function getHeader($header): array
  235. {
  236. $header = \strtolower($header);
  237. if (!isset($this->headerNames[$header])) {
  238. return [];
  239. }
  240. $header = $this->headerNames[$header];
  241. return $this->headers[$header];
  242. }
  243. public function getHeaderLine($header): string
  244. {
  245. return \implode(', ', $this->getHeader($header));
  246. }
  247. public function withHeader($header, $value): self
  248. {
  249. $value = $this->validateAndTrimHeader($header, $value);
  250. $normalized = \strtolower($header);
  251. $new = clone $this;
  252. if (isset($new->headerNames[$normalized])) {
  253. unset($new->headers[$new->headerNames[$normalized]]);
  254. }
  255. $new->headerNames[$normalized] = $header;
  256. $new->headers[$header] = $value;
  257. return $new;
  258. }
  259. public function withAddedHeader($header, $value): self
  260. {
  261. if (!\is_string($header) || '' === $header) {
  262. throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
  263. }
  264. $new = clone $this;
  265. $new->setHeaders([$header => $value]);
  266. return $new;
  267. }
  268. public function withoutHeader($header): self
  269. {
  270. $normalized = \strtolower($header);
  271. if (!isset($this->headerNames[$normalized])) {
  272. return $this;
  273. }
  274. $header = $this->headerNames[$normalized];
  275. $new = clone $this;
  276. unset($new->headers[$header], $new->headerNames[$normalized]);
  277. return $new;
  278. }
  279. public function getBody(): StreamInterface
  280. {
  281. if (null === $this->stream) {
  282. $this->stream = Stream::create('');
  283. }
  284. return $this->stream;
  285. }
  286. public function withBody(StreamInterface $body): self
  287. {
  288. if ($body === $this->stream) {
  289. return $this;
  290. }
  291. $new = clone $this;
  292. $new->stream = $body;
  293. return $new;
  294. }
  295. private function setHeaders(array $headers) /*:void*/
  296. {
  297. foreach ($headers as $header => $value) {
  298. $value = $this->validateAndTrimHeader($header, $value);
  299. $normalized = \strtolower($header);
  300. if (isset($this->headerNames[$normalized])) {
  301. $header = $this->headerNames[$normalized];
  302. $this->headers[$header] = \array_merge($this->headers[$header], $value);
  303. } else {
  304. $this->headerNames[$normalized] = $header;
  305. $this->headers[$header] = $value;
  306. }
  307. }
  308. }
  309. private function validateAndTrimHeader($header, $values): array
  310. {
  311. if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) {
  312. throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.');
  313. }
  314. if (!\is_array($values)) {
  315. if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) {
  316. throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
  317. }
  318. return [\trim((string) $values, " \t")];
  319. }
  320. if (empty($values)) {
  321. throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given.');
  322. }
  323. $returnValues = [];
  324. foreach ($values as $v) {
  325. if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) {
  326. throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.');
  327. }
  328. $returnValues[] = \trim((string) $v, " \t");
  329. }
  330. return $returnValues;
  331. }
  332. }
  333. // file: vendor/nyholm/psr7/src/Request.php
  334. final class Request implements RequestInterface
  335. {
  336. use MessageTrait;
  337. use RequestTrait;
  338. public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
  339. {
  340. if (!($uri instanceof UriInterface)) {
  341. $uri = new Uri($uri);
  342. }
  343. $this->method = $method;
  344. $this->uri = $uri;
  345. $this->setHeaders($headers);
  346. $this->protocol = $version;
  347. if (!$this->hasHeader('Host')) {
  348. $this->updateHostFromUri();
  349. }
  350. if ('' !== $body && null !== $body) {
  351. $this->stream = Stream::create($body);
  352. }
  353. }
  354. }
  355. // file: vendor/nyholm/psr7/src/RequestTrait.php
  356. trait RequestTrait
  357. {
  358. private $method;
  359. private $requestTarget;
  360. private $uri;
  361. public function getRequestTarget(): string
  362. {
  363. if (null !== $this->requestTarget) {
  364. return $this->requestTarget;
  365. }
  366. if ('' === $target = $this->uri->getPath()) {
  367. $target = '/';
  368. }
  369. if ('' !== $this->uri->getQuery()) {
  370. $target .= '?' . $this->uri->getQuery();
  371. }
  372. return $target;
  373. }
  374. public function withRequestTarget($requestTarget): self
  375. {
  376. if (\preg_match('#\s#', $requestTarget)) {
  377. throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace');
  378. }
  379. $new = clone $this;
  380. $new->requestTarget = $requestTarget;
  381. return $new;
  382. }
  383. public function getMethod(): string
  384. {
  385. return $this->method;
  386. }
  387. public function withMethod($method): self
  388. {
  389. if (!\is_string($method)) {
  390. throw new \InvalidArgumentException('Method must be a string');
  391. }
  392. $new = clone $this;
  393. $new->method = $method;
  394. return $new;
  395. }
  396. public function getUri(): UriInterface
  397. {
  398. return $this->uri;
  399. }
  400. public function withUri(UriInterface $uri, $preserveHost = false): self
  401. {
  402. if ($uri === $this->uri) {
  403. return $this;
  404. }
  405. $new = clone $this;
  406. $new->uri = $uri;
  407. if (!$preserveHost || !$this->hasHeader('Host')) {
  408. $new->updateHostFromUri();
  409. }
  410. return $new;
  411. }
  412. private function updateHostFromUri() /*:void*/
  413. {
  414. if ('' === $host = $this->uri->getHost()) {
  415. return;
  416. }
  417. if (null !== ($port = $this->uri->getPort())) {
  418. $host .= ':' . $port;
  419. }
  420. if (isset($this->headerNames['host'])) {
  421. $header = $this->headerNames['host'];
  422. } else {
  423. $this->headerNames['host'] = $header = 'Host';
  424. }
  425. $this->headers = [$header => [$host]] + $this->headers;
  426. }
  427. }
  428. // file: vendor/nyholm/psr7/src/Response.php
  429. final class Response implements ResponseInterface
  430. {
  431. use MessageTrait;
  432. /*private*/ const PHRASES = [
  433. 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing',
  434. 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported',
  435. 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect',
  436. 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons',
  437. 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required',
  438. ];
  439. private $reasonPhrase = '';
  440. private $statusCode;
  441. public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', string $reason = null)
  442. {
  443. if ('' !== $body && null !== $body) {
  444. $this->stream = Stream::create($body);
  445. }
  446. $this->statusCode = $status;
  447. $this->setHeaders($headers);
  448. if (null === $reason && isset(self::PHRASES[$this->statusCode])) {
  449. $this->reasonPhrase = self::PHRASES[$status];
  450. } else {
  451. $this->reasonPhrase = $reason;
  452. }
  453. $this->protocol = $version;
  454. }
  455. public function getStatusCode(): int
  456. {
  457. return $this->statusCode;
  458. }
  459. public function getReasonPhrase(): string
  460. {
  461. return $this->reasonPhrase;
  462. }
  463. public function withStatus($code, $reasonPhrase = ''): self
  464. {
  465. if (!\is_int($code) && !\is_string($code)) {
  466. throw new \InvalidArgumentException('Status code has to be an integer');
  467. }
  468. $code = (int) $code;
  469. if ($code < 100 || $code > 599) {
  470. throw new \InvalidArgumentException('Status code has to be an integer between 100 and 599');
  471. }
  472. $new = clone $this;
  473. $new->statusCode = $code;
  474. if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) {
  475. $reasonPhrase = self::PHRASES[$new->statusCode];
  476. }
  477. $new->reasonPhrase = $reasonPhrase;
  478. return $new;
  479. }
  480. }
  481. // file: vendor/nyholm/psr7/src/ServerRequest.php
  482. final class ServerRequest implements ServerRequestInterface
  483. {
  484. use MessageTrait;
  485. use RequestTrait;
  486. private $attributes = [];
  487. private $cookieParams = [];
  488. private $parsedBody;
  489. private $queryParams = [];
  490. private $serverParams;
  491. private $uploadedFiles = [];
  492. public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = [])
  493. {
  494. $this->serverParams = $serverParams;
  495. if (!($uri instanceof UriInterface)) {
  496. $uri = new Uri($uri);
  497. }
  498. $this->method = $method;
  499. $this->uri = $uri;
  500. $this->setHeaders($headers);
  501. $this->protocol = $version;
  502. if (!$this->hasHeader('Host')) {
  503. $this->updateHostFromUri();
  504. }
  505. if ('' !== $body && null !== $body) {
  506. $this->stream = Stream::create($body);
  507. }
  508. }
  509. public function getServerParams(): array
  510. {
  511. return $this->serverParams;
  512. }
  513. public function getUploadedFiles(): array
  514. {
  515. return $this->uploadedFiles;
  516. }
  517. public function withUploadedFiles(array $uploadedFiles)
  518. {
  519. $new = clone $this;
  520. $new->uploadedFiles = $uploadedFiles;
  521. return $new;
  522. }
  523. public function getCookieParams(): array
  524. {
  525. return $this->cookieParams;
  526. }
  527. public function withCookieParams(array $cookies)
  528. {
  529. $new = clone $this;
  530. $new->cookieParams = $cookies;
  531. return $new;
  532. }
  533. public function getQueryParams(): array
  534. {
  535. return $this->queryParams;
  536. }
  537. public function withQueryParams(array $query)
  538. {
  539. $new = clone $this;
  540. $new->queryParams = $query;
  541. return $new;
  542. }
  543. public function getParsedBody()
  544. {
  545. return $this->parsedBody;
  546. }
  547. public function withParsedBody($data)
  548. {
  549. if (!\is_array($data) && !\is_object($data) && null !== $data) {
  550. throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null');
  551. }
  552. $new = clone $this;
  553. $new->parsedBody = $data;
  554. return $new;
  555. }
  556. public function getAttributes(): array
  557. {
  558. return $this->attributes;
  559. }
  560. public function getAttribute($attribute, $default = null)
  561. {
  562. if (false === \array_key_exists($attribute, $this->attributes)) {
  563. return $default;
  564. }
  565. return $this->attributes[$attribute];
  566. }
  567. public function withAttribute($attribute, $value): self
  568. {
  569. $new = clone $this;
  570. $new->attributes[$attribute] = $value;
  571. return $new;
  572. }
  573. public function withoutAttribute($attribute): self
  574. {
  575. if (false === \array_key_exists($attribute, $this->attributes)) {
  576. return $this;
  577. }
  578. $new = clone $this;
  579. unset($new->attributes[$attribute]);
  580. return $new;
  581. }
  582. }
  583. // file: vendor/nyholm/psr7/src/Stream.php
  584. final class Stream implements StreamInterface
  585. {
  586. private $stream;
  587. private $seekable;
  588. private $readable;
  589. private $writable;
  590. private $uri;
  591. private $size;
  592. /*private*/ const READ_WRITE_HASH = [
  593. 'read' => [
  594. 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
  595. 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
  596. 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
  597. 'x+t' => true, 'c+t' => true, 'a+' => true,
  598. ],
  599. 'write' => [
  600. 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
  601. 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
  602. 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
  603. 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
  604. ],
  605. ];
  606. private function __construct()
  607. {
  608. }
  609. public static function create($body = ''): StreamInterface
  610. {
  611. if ($body instanceof StreamInterface) {
  612. return $body;
  613. }
  614. if (\is_string($body)) {
  615. $resource = \fopen('php://temp', 'rw+');
  616. \fwrite($resource, $body);
  617. $body = $resource;
  618. }
  619. if (\is_resource($body)) {
  620. $new = new self();
  621. $new->stream = $body;
  622. $meta = \stream_get_meta_data($new->stream);
  623. $new->seekable = $meta['seekable'];
  624. $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
  625. $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
  626. $new->uri = $new->getMetadata('uri');
  627. return $new;
  628. }
  629. throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.');
  630. }
  631. public function __destruct()
  632. {
  633. $this->close();
  634. }
  635. public function __toString(): string
  636. {
  637. try {
  638. if ($this->isSeekable()) {
  639. $this->seek(0);
  640. }
  641. return $this->getContents();
  642. } catch (\Exception $e) {
  643. return '';
  644. }
  645. }
  646. public function close() /*:void*/
  647. {
  648. if (isset($this->stream)) {
  649. if (\is_resource($this->stream)) {
  650. \fclose($this->stream);
  651. }
  652. $this->detach();
  653. }
  654. }
  655. public function detach()
  656. {
  657. if (!isset($this->stream)) {
  658. return null;
  659. }
  660. $result = $this->stream;
  661. unset($this->stream);
  662. $this->size = $this->uri = null;
  663. $this->readable = $this->writable = $this->seekable = false;
  664. return $result;
  665. }
  666. public function getSize() /*:?int*/
  667. {
  668. if (null !== $this->size) {
  669. return $this->size;
  670. }
  671. if (!isset($this->stream)) {
  672. return null;
  673. }
  674. if ($this->uri) {
  675. \clearstatcache(true, $this->uri);
  676. }
  677. $stats = \fstat($this->stream);
  678. if (isset($stats['size'])) {
  679. $this->size = $stats['size'];
  680. return $this->size;
  681. }
  682. return null;
  683. }
  684. public function tell(): int
  685. {
  686. if (false === $result = \ftell($this->stream)) {
  687. throw new \RuntimeException('Unable to determine stream position');
  688. }
  689. return $result;
  690. }
  691. public function eof(): bool
  692. {
  693. return !$this->stream || \feof($this->stream);
  694. }
  695. public function isSeekable(): bool
  696. {
  697. return $this->seekable;
  698. }
  699. public function seek($offset, $whence = \SEEK_SET) /*:void*/
  700. {
  701. if (!$this->seekable) {
  702. throw new \RuntimeException('Stream is not seekable');
  703. }
  704. if (-1 === \fseek($this->stream, $offset, $whence)) {
  705. throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, true));
  706. }
  707. }
  708. public function rewind() /*:void*/
  709. {
  710. $this->seek(0);
  711. }
  712. public function isWritable(): bool
  713. {
  714. return $this->writable;
  715. }
  716. public function write($string): int
  717. {
  718. if (!$this->writable) {
  719. throw new \RuntimeException('Cannot write to a non-writable stream');
  720. }
  721. $this->size = null;
  722. if (false === $result = \fwrite($this->stream, $string)) {
  723. throw new \RuntimeException('Unable to write to stream');
  724. }
  725. return $result;
  726. }
  727. public function isReadable(): bool
  728. {
  729. return $this->readable;
  730. }
  731. public function read($length): string
  732. {
  733. if (!$this->readable) {
  734. throw new \RuntimeException('Cannot read from non-readable stream');
  735. }
  736. return \fread($this->stream, $length);
  737. }
  738. public function getContents(): string
  739. {
  740. if (!isset($this->stream)) {
  741. throw new \RuntimeException('Unable to read stream contents');
  742. }
  743. if (false === $contents = \stream_get_contents($this->stream)) {
  744. throw new \RuntimeException('Unable to read stream contents');
  745. }
  746. return $contents;
  747. }
  748. public function getMetadata($key = null)
  749. {
  750. if (!isset($this->stream)) {
  751. return $key ? null : [];
  752. }
  753. $meta = \stream_get_meta_data($this->stream);
  754. if (null === $key) {
  755. return $meta;
  756. }
  757. return $meta[$key] ?? null;
  758. }
  759. }
  760. // file: vendor/nyholm/psr7/src/UploadedFile.php
  761. final class UploadedFile implements UploadedFileInterface
  762. {
  763. /*private*/ const ERRORS = [
  764. \UPLOAD_ERR_OK => 1,
  765. \UPLOAD_ERR_INI_SIZE => 1,
  766. \UPLOAD_ERR_FORM_SIZE => 1,
  767. \UPLOAD_ERR_PARTIAL => 1,
  768. \UPLOAD_ERR_NO_FILE => 1,
  769. \UPLOAD_ERR_NO_TMP_DIR => 1,
  770. \UPLOAD_ERR_CANT_WRITE => 1,
  771. \UPLOAD_ERR_EXTENSION => 1,
  772. ];
  773. private $clientFilename;
  774. private $clientMediaType;
  775. private $error;
  776. private $file;
  777. private $moved = false;
  778. private $size;
  779. private $stream;
  780. public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null)
  781. {
  782. if (false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) {
  783. throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants.');
  784. }
  785. if (false === \is_int($size)) {
  786. throw new \InvalidArgumentException('Upload file size must be an integer');
  787. }
  788. if (null !== $clientFilename && !\is_string($clientFilename)) {
  789. throw new \InvalidArgumentException('Upload file client filename must be a string or null');
  790. }
  791. if (null !== $clientMediaType && !\is_string($clientMediaType)) {
  792. throw new \InvalidArgumentException('Upload file client media type must be a string or null');
  793. }
  794. $this->error = $errorStatus;
  795. $this->size = $size;
  796. $this->clientFilename = $clientFilename;
  797. $this->clientMediaType = $clientMediaType;
  798. if (\UPLOAD_ERR_OK === $this->error) {
  799. if (\is_string($streamOrFile)) {
  800. $this->file = $streamOrFile;
  801. } elseif (\is_resource($streamOrFile)) {
  802. $this->stream = Stream::create($streamOrFile);
  803. } elseif ($streamOrFile instanceof StreamInterface) {
  804. $this->stream = $streamOrFile;
  805. } else {
  806. throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile');
  807. }
  808. }
  809. }
  810. private function validateActive() /*:void*/
  811. {
  812. if (\UPLOAD_ERR_OK !== $this->error) {
  813. throw new \RuntimeException('Cannot retrieve stream due to upload error');
  814. }
  815. if ($this->moved) {
  816. throw new \RuntimeException('Cannot retrieve stream after it has already been moved');
  817. }
  818. }
  819. public function getStream(): StreamInterface
  820. {
  821. $this->validateActive();
  822. if ($this->stream instanceof StreamInterface) {
  823. return $this->stream;
  824. }
  825. $resource = \fopen($this->file, 'r');
  826. return Stream::create($resource);
  827. }
  828. public function moveTo($targetPath) /*:void*/
  829. {
  830. $this->validateActive();
  831. if (!\is_string($targetPath) || '' === $targetPath) {
  832. throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
  833. }
  834. if (null !== $this->file) {
  835. $this->moved = 'cli' === \PHP_SAPI ? \rename($this->file, $targetPath) : \move_uploaded_file($this->file, $targetPath);
  836. } else {
  837. $stream = $this->getStream();
  838. if ($stream->isSeekable()) {
  839. $stream->rewind();
  840. }
  841. $dest = Stream::create(\fopen($targetPath, 'w'));
  842. while (!$stream->eof()) {
  843. if (!$dest->write($stream->read(1048576))) {
  844. break;
  845. }
  846. }
  847. $this->moved = true;
  848. }
  849. if (false === $this->moved) {
  850. throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath));
  851. }
  852. }
  853. public function getSize(): int
  854. {
  855. return $this->size;
  856. }
  857. public function getError(): int
  858. {
  859. return $this->error;
  860. }
  861. public function getClientFilename() /*:?string*/
  862. {
  863. return $this->clientFilename;
  864. }
  865. public function getClientMediaType() /*:?string*/
  866. {
  867. return $this->clientMediaType;
  868. }
  869. }
  870. // file: vendor/nyholm/psr7/src/Uri.php
  871. final class Uri implements UriInterface
  872. {
  873. /*private*/ const SCHEMES = ['http' => 80, 'https' => 443];
  874. /*private*/ const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
  875. /*private*/ const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
  876. private $scheme = '';
  877. private $userInfo = '';
  878. private $host = '';
  879. private $port;
  880. private $path = '';
  881. private $query = '';
  882. private $fragment = '';
  883. public function __construct(string $uri = '')
  884. {
  885. if ('' !== $uri) {
  886. if (false === $parts = \parse_url($uri)) {
  887. throw new \InvalidArgumentException("Unable to parse URI: $uri");
  888. }
  889. $this->scheme = isset($parts['scheme']) ? \strtolower($parts['scheme']) : '';
  890. $this->userInfo = $parts['user'] ?? '';
  891. $this->host = isset($parts['host']) ? \strtolower($parts['host']) : '';
  892. $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
  893. $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
  894. $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
  895. $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
  896. if (isset($parts['pass'])) {
  897. $this->userInfo .= ':' . $parts['pass'];
  898. }
  899. }
  900. }
  901. public function __toString(): string
  902. {
  903. return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
  904. }
  905. public function getScheme(): string
  906. {
  907. return $this->scheme;
  908. }
  909. public function getAuthority(): string
  910. {
  911. if ('' === $this->host) {
  912. return '';
  913. }
  914. $authority = $this->host;
  915. if ('' !== $this->userInfo) {
  916. $authority = $this->userInfo . '@' . $authority;
  917. }
  918. if (null !== $this->port) {
  919. $authority .= ':' . $this->port;
  920. }
  921. return $authority;
  922. }
  923. public function getUserInfo(): string
  924. {
  925. return $this->userInfo;
  926. }
  927. public function getHost(): string
  928. {
  929. return $this->host;
  930. }
  931. public function getPort() /*:?int*/
  932. {
  933. return $this->port;
  934. }
  935. public function getPath(): string
  936. {
  937. return $this->path;
  938. }
  939. public function getQuery(): string
  940. {
  941. return $this->query;
  942. }
  943. public function getFragment(): string
  944. {
  945. return $this->fragment;
  946. }
  947. public function withScheme($scheme): self
  948. {
  949. if (!\is_string($scheme)) {
  950. throw new \InvalidArgumentException('Scheme must be a string');
  951. }
  952. if ($this->scheme === $scheme = \strtolower($scheme)) {
  953. return $this;
  954. }
  955. $new = clone $this;
  956. $new->scheme = $scheme;
  957. $new->port = $new->filterPort($new->port);
  958. return $new;
  959. }
  960. public function withUserInfo($user, $password = null): self
  961. {
  962. $info = $user;
  963. if (null !== $password && '' !== $password) {
  964. $info .= ':' . $password;
  965. }
  966. if ($this->userInfo === $info) {
  967. return $this;
  968. }
  969. $new = clone $this;
  970. $new->userInfo = $info;
  971. return $new;
  972. }
  973. public function withHost($host): self
  974. {
  975. if (!\is_string($host)) {
  976. throw new \InvalidArgumentException('Host must be a string');
  977. }
  978. if ($this->host === $host = \strtolower($host)) {
  979. return $this;
  980. }
  981. $new = clone $this;
  982. $new->host = $host;
  983. return $new;
  984. }
  985. public function withPort($port): self
  986. {
  987. if ($this->port === $port = $this->filterPort($port)) {
  988. return $this;
  989. }
  990. $new = clone $this;
  991. $new->port = $port;
  992. return $new;
  993. }
  994. public function withPath($path): self
  995. {
  996. if ($this->path === $path = $this->filterPath($path)) {
  997. return $this;
  998. }
  999. $new = clone $this;
  1000. $new->path = $path;
  1001. return $new;
  1002. }
  1003. public function withQuery($query): self
  1004. {
  1005. if ($this->query === $query = $this->filterQueryAndFragment($query)) {
  1006. return $this;
  1007. }
  1008. $new = clone $this;
  1009. $new->query = $query;
  1010. return $new;
  1011. }
  1012. public function withFragment($fragment): self
  1013. {
  1014. if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) {
  1015. return $this;
  1016. }
  1017. $new = clone $this;
  1018. $new->fragment = $fragment;
  1019. return $new;
  1020. }
  1021. private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string
  1022. {
  1023. $uri = '';
  1024. if ('' !== $scheme) {
  1025. $uri .= $scheme . ':';
  1026. }
  1027. if ('' !== $authority) {
  1028. $uri .= '//' . $authority;
  1029. }
  1030. if ('' !== $path) {
  1031. if ('/' !== $path[0]) {
  1032. if ('' !== $authority) {
  1033. $path = '/' . $path;
  1034. }
  1035. } elseif (isset($path[1]) && '/' === $path[1]) {
  1036. if ('' === $authority) {
  1037. $path = '/' . \ltrim($path, '/');
  1038. }
  1039. }
  1040. $uri .= $path;
  1041. }
  1042. if ('' !== $query) {
  1043. $uri .= '?' . $query;
  1044. }
  1045. if ('' !== $fragment) {
  1046. $uri .= '#' . $fragment;
  1047. }
  1048. return $uri;
  1049. }
  1050. private static function isNonStandardPort(string $scheme, int $port): bool
  1051. {
  1052. return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme];
  1053. }
  1054. private function filterPort($port) /*:?int*/
  1055. {
  1056. if (null === $port) {
  1057. return null;
  1058. }
  1059. $port = (int) $port;
  1060. if (1 > $port || 0xffff < $port) {
  1061. throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 1 and 65535', $port));
  1062. }
  1063. return self::isNonStandardPort($this->scheme, $port) ? $port : null;
  1064. }
  1065. private function filterPath($path): string
  1066. {
  1067. if (!\is_string($path)) {
  1068. throw new \InvalidArgumentException('Path must be a string');
  1069. }
  1070. return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path);
  1071. }
  1072. private function filterQueryAndFragment($str): string
  1073. {
  1074. if (!\is_string($str)) {
  1075. throw new \InvalidArgumentException('Query and fragment must be a string');
  1076. }
  1077. return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
  1078. }
  1079. private static function rawurlencodeMatchZero(array $match): string
  1080. {
  1081. return \rawurlencode($match[0]);
  1082. }
  1083. }
  1084. // file: vendor/nyholm/psr7-server/src/ServerRequestCreator.php
  1085. final class ServerRequestCreator implements ServerRequestCreatorInterface
  1086. {
  1087. private $serverRequestFactory;
  1088. private $uriFactory;
  1089. private $uploadedFileFactory;
  1090. private $streamFactory;
  1091. public function __construct(
  1092. ServerRequestFactoryInterface $serverRequestFactory,
  1093. UriFactoryInterface $uriFactory,
  1094. UploadedFileFactoryInterface $uploadedFileFactory,
  1095. StreamFactoryInterface $streamFactory
  1096. ) {
  1097. $this->serverRequestFactory = $serverRequestFactory;
  1098. $this->uriFactory = $uriFactory;
  1099. $this->uploadedFileFactory = $uploadedFileFactory;
  1100. $this->streamFactory = $streamFactory;
  1101. }
  1102. public function fromGlobals(): ServerRequestInterface
  1103. {
  1104. $server = $_SERVER;
  1105. if (false === isset($server['REQUEST_METHOD'])) {
  1106. $server['REQUEST_METHOD'] = 'GET';
  1107. }
  1108. $headers = \function_exists('getallheaders') ? getallheaders() : static::getHeadersFromServer($_SERVER);
  1109. return $this->fromArrays($server, $headers, $_COOKIE, $_GET, $_POST, $_FILES, fopen('php://input', 'r') ?: null);
  1110. }
  1111. public function fromArrays(array $server, array $headers = [], array $cookie = [], array $get = [], array $post = [], array $files = [], $body = null): ServerRequestInterface
  1112. {
  1113. $method = $this->getMethodFromEnv($server);
  1114. $uri = $this->getUriFromEnvWithHTTP($server);
  1115. $protocol = isset($server['SERVER_PROTOCOL']) ? \str_replace('HTTP/', '', $server['SERVER_PROTOCOL']) : '1.1';
  1116. $serverRequest = $this->serverRequestFactory->createServerRequest($method, $uri, $server);
  1117. foreach ($headers as $name => $value) {
  1118. $serverRequest = $serverRequest->withAddedHeader($name, $value);
  1119. }
  1120. $serverRequest = $serverRequest
  1121. ->withProtocolVersion($protocol)
  1122. ->withCookieParams($cookie)
  1123. ->withQueryParams($get)
  1124. ->withParsedBody($post)
  1125. ->withUploadedFiles($this->normalizeFiles($files));
  1126. if (null === $body) {
  1127. return $serverRequest;
  1128. }
  1129. if (\is_resource($body)) {
  1130. $body = $this->streamFactory->createStreamFromResource($body);
  1131. } elseif (\is_string($body)) {
  1132. $body = $this->streamFactory->createStream($body);
  1133. } elseif (!$body instanceof StreamInterface) {
  1134. throw new \InvalidArgumentException('The $body parameter to ServerRequestCreator::fromArrays must be string, resource or StreamInterface');
  1135. }
  1136. return $serverRequest->withBody($body);
  1137. }
  1138. public static function getHeadersFromServer(array $server): array
  1139. {
  1140. $headers = [];
  1141. foreach ($server as $key => $value) {
  1142. if (0 === \strpos($key, 'REDIRECT_')) {
  1143. $key = \substr($key, 9);
  1144. if (\array_key_exists($key, $server)) {
  1145. continue;
  1146. }
  1147. }
  1148. if ($value && 0 === \strpos($key, 'HTTP_')) {
  1149. $name = \strtr(\strtolower(\substr($key, 5)), '_', '-');
  1150. $headers[$name] = $value;
  1151. continue;
  1152. }
  1153. if ($value && 0 === \strpos($key, 'CONTENT_')) {
  1154. $name = 'content-'.\strtolower(\substr($key, 8));
  1155. $headers[$name] = $value;
  1156. continue;
  1157. }
  1158. }
  1159. return $headers;
  1160. }
  1161. private function getMethodFromEnv(array $environment): string
  1162. {
  1163. if (false === isset($environment['REQUEST_METHOD'])) {
  1164. throw new \InvalidArgumentException('Cannot determine HTTP method');
  1165. }
  1166. return $environment['REQUEST_METHOD'];
  1167. }
  1168. private function getUriFromEnvWithHTTP(array $environment): UriInterface
  1169. {
  1170. $uri = $this->createUriFromArray($environment);
  1171. if (empty($uri->getScheme())) {
  1172. $uri = $uri->withScheme('http');
  1173. }
  1174. return $uri;
  1175. }
  1176. private function normalizeFiles(array $files): array
  1177. {
  1178. $normalized = [];
  1179. foreach ($files as $key => $value) {
  1180. if ($value instanceof UploadedFileInterface) {
  1181. $normalized[$key] = $value;
  1182. } elseif (\is_array($value) && isset($value['tmp_name'])) {
  1183. $normalized[$key] = $this->createUploadedFileFromSpec($value);
  1184. } elseif (\is_array($value)) {
  1185. $normalized[$key] = $this->normalizeFiles($value);
  1186. } else {
  1187. throw new \InvalidArgumentException('Invalid value in files specification');
  1188. }
  1189. }
  1190. return $normalized;
  1191. }
  1192. private function createUploadedFileFromSpec(array $value)
  1193. {
  1194. if (\is_array($value['tmp_name'])) {
  1195. return $this->normalizeNestedFileSpec($value);
  1196. }
  1197. try {
  1198. $stream = $this->streamFactory->createStreamFromFile($value['tmp_name']);
  1199. } catch (\RuntimeException $e) {
  1200. $stream = $this->streamFactory->createStream();
  1201. }
  1202. return $this->uploadedFileFactory->createUploadedFile(
  1203. $stream,
  1204. (int) $value['size'],
  1205. (int) $value['error'],
  1206. $value['name'],
  1207. $value['type']
  1208. );
  1209. }
  1210. private function normalizeNestedFileSpec(array $files = []): array
  1211. {
  1212. $normalizedFiles = [];
  1213. foreach (\array_keys($files['tmp_name']) as $key) {
  1214. $spec = [
  1215. 'tmp_name' => $files['tmp_name'][$key],
  1216. 'size' => $files['size'][$key],
  1217. 'error' => $files['error'][$key],
  1218. 'name' => $files['name'][$key],
  1219. 'type' => $files['type'][$key],
  1220. ];
  1221. $normalizedFiles[$key] = $this->createUploadedFileFromSpec($spec);
  1222. }
  1223. return $normalizedFiles;
  1224. }
  1225. private function createUriFromArray(array $server): UriInterface
  1226. {
  1227. $uri = $this->uriFactory->createUri('');
  1228. if (isset($server['REQUEST_SCHEME'])) {
  1229. $uri = $uri->withScheme($server['REQUEST_SCHEME']);
  1230. } elseif (isset($server['HTTPS'])) {
  1231. $uri = $uri->withScheme('on' === $server['HTTPS'] ? 'https' : 'http');
  1232. }
  1233. if (isset($server['HTTP_HOST'])) {
  1234. $uri = $uri->withHost($server['HTTP_HOST']);
  1235. } elseif (isset($server['SERVER_NAME'])) {
  1236. $uri = $uri->withHost($server['SERVER_NAME']);
  1237. }
  1238. if (isset($server['SERVER_PORT'])) {
  1239. $uri = $uri->withPort($server['SERVER_PORT']);
  1240. }
  1241. if (isset($server['REQUEST_URI'])) {
  1242. $uri = $uri->withPath(\current(\explode('?', $server['REQUEST_URI'])));
  1243. }
  1244. if (isset($server['QUERY_STRING'])) {
  1245. $uri = $uri->withQuery($server['QUERY_STRING']);
  1246. }
  1247. return $uri;
  1248. }
  1249. }
  1250. // file: vendor/nyholm/psr7-server/src/ServerRequestCreatorInterface.php
  1251. interface ServerRequestCreatorInterface
  1252. {
  1253. public function fromGlobals(): ServerRequestInterface;
  1254. public function fromArrays(
  1255. array $server,
  1256. array $headers = [],
  1257. array $cookie = [],
  1258. array $get = [],
  1259. array $post = [],
  1260. array $files = [],
  1261. $body = null
  1262. ): ServerRequestInterface;
  1263. public static function getHeadersFromServer(array $server): array;
  1264. }
  1265. // file: src/Tqdev/PhpCrudApi/Cache/Cache.php
  1266. interface Cache
  1267. {
  1268. public function set(string $key, string $value, int $ttl = 0): bool;
  1269. public function get(string $key): string;
  1270. public function clear(): bool;
  1271. }
  1272. // file: src/Tqdev/PhpCrudApi/Cache/CacheFactory.php
  1273. class CacheFactory
  1274. {
  1275. const PREFIX = 'phpcrudapi-%s-%s-%s-';
  1276. private static function getPrefix(Config $config): string
  1277. {
  1278. $driver = $config->getDriver();
  1279. $database = $config->getDatabase();
  1280. $filehash = substr(md5(__FILE__), 0, 8);
  1281. return sprintf(self::PREFIX, $driver, $database, $filehash);
  1282. }
  1283. public static function create(Config $config): Cache
  1284. {
  1285. switch ($config->getCacheType()) {
  1286. case 'TempFile':
  1287. $cache = new TempFileCache(self::getPrefix($config), $config->getCachePath());
  1288. break;
  1289. case 'Redis':
  1290. $cache = new RedisCache(self::getPrefix($config), $config->getCachePath());
  1291. break;
  1292. case 'Memcache':
  1293. $cache = new MemcacheCache(self::getPrefix($config), $config->getCachePath());
  1294. break;
  1295. case 'Memcached':
  1296. $cache = new MemcachedCache(self::getPrefix($config), $config->getCachePath());
  1297. break;
  1298. default:
  1299. $cache = new NoCache();
  1300. }
  1301. return $cache;
  1302. }
  1303. }
  1304. // file: src/Tqdev/PhpCrudApi/Cache/MemcacheCache.php
  1305. class MemcacheCache implements Cache
  1306. {
  1307. protected $prefix;
  1308. protected $memcache;
  1309. public function __construct(string $prefix, string $config)
  1310. {
  1311. $this->prefix = $prefix;
  1312. if ($config == '') {
  1313. $address = 'localhost';
  1314. $port = 11211;
  1315. } elseif (strpos($config, ':') === false) {
  1316. $address = $config;
  1317. $port = 11211;
  1318. } else {
  1319. list($address, $port) = explode(':', $config);
  1320. }
  1321. $this->memcache = $this->create();
  1322. $this->memcache->addServer($address, $port);
  1323. }
  1324. protected function create() /*: \Memcache*/
  1325. {
  1326. return new \Memcache();
  1327. }
  1328. public function set(string $key, string $value, int $ttl = 0): bool
  1329. {
  1330. return $this->memcache->set($this->prefix . $key, $value, 0, $ttl);
  1331. }
  1332. public function get(string $key): string
  1333. {
  1334. return $this->memcache->get($this->prefix . $key) ?: '';
  1335. }
  1336. public function clear(): bool
  1337. {
  1338. return $this->memcache->flush();
  1339. }
  1340. }
  1341. // file: src/Tqdev/PhpCrudApi/Cache/MemcachedCache.php
  1342. class MemcachedCache extends MemcacheCache
  1343. {
  1344. protected function create() /*: \Memcached*/
  1345. {
  1346. return new \Memcached();
  1347. }
  1348. public function set(string $key, string $value, int $ttl = 0): bool
  1349. {
  1350. return $this->memcache->set($this->prefix . $key, $value, $ttl);
  1351. }
  1352. }
  1353. // file: src/Tqdev/PhpCrudApi/Cache/NoCache.php
  1354. class NoCache implements Cache
  1355. {
  1356. public function __construct()
  1357. {
  1358. }
  1359. public function set(string $key, string $value, int $ttl = 0): bool
  1360. {
  1361. return true;
  1362. }
  1363. public function get(string $key): string
  1364. {
  1365. return '';
  1366. }
  1367. public function clear(): bool
  1368. {
  1369. return true;
  1370. }
  1371. }
  1372. // file: src/Tqdev/PhpCrudApi/Cache/RedisCache.php
  1373. class RedisCache implements Cache
  1374. {
  1375. protected $prefix;
  1376. protected $redis;
  1377. public function __construct(string $prefix, string $config)
  1378. {
  1379. $this->prefix = $prefix;
  1380. if ($config == '') {
  1381. $config = '127.0.0.1';
  1382. }
  1383. $params = explode(':', $config, 6);
  1384. if (isset($params[3])) {
  1385. $params[3] = null;
  1386. }
  1387. $this->redis = new \Redis();
  1388. call_user_func_array(array($this->redis, 'pconnect'), $params);
  1389. }
  1390. public function set(string $key, string $value, int $ttl = 0): bool
  1391. {
  1392. return $this->redis->set($this->prefix . $key, $value, $ttl);
  1393. }
  1394. public function get(string $key): string
  1395. {
  1396. return $this->redis->get($this->prefix . $key) ?: '';
  1397. }
  1398. public function clear(): bool
  1399. {
  1400. return $this->redis->flushDb();
  1401. }
  1402. }
  1403. // file: src/Tqdev/PhpCrudApi/Cache/TempFileCache.php
  1404. class TempFileCache implements Cache
  1405. {
  1406. const SUFFIX = 'cache';
  1407. private $path;
  1408. private $segments;
  1409. public function __construct(string $prefix, string $config)
  1410. {
  1411. $this->segments = [];
  1412. $s = DIRECTORY_SEPARATOR;
  1413. $ps = PATH_SEPARATOR;
  1414. if ($config == '') {
  1415. $this->path = sys_get_temp_dir() . $s . $prefix . self::SUFFIX;
  1416. } elseif (strpos($config, $ps) === false) {
  1417. $this->path = $config;
  1418. } else {
  1419. list($path, $segments) = explode($ps, $config);
  1420. $this->path = $path;
  1421. $this->segments = explode(',', $segments);
  1422. }
  1423. if (file_exists($this->path) && is_dir($this->path)) {
  1424. $this->clean($this->path, array_filter($this->segments), strlen(md5('')), false);
  1425. }
  1426. }
  1427. private function getFileName(string $key): string
  1428. {
  1429. $s = DIRECTORY_SEPARATOR;
  1430. $md5 = md5($key);
  1431. $filename = rtrim($this->path, $s) . $s;
  1432. $i = 0;
  1433. foreach ($this->segments as $segment) {
  1434. $filename .= substr($md5, $i, $segment) . $s;
  1435. $i += $segment;
  1436. }
  1437. $filename .= substr($md5, $i);
  1438. return $filename;
  1439. }
  1440. public function set(string $key, string $value, int $ttl = 0): bool
  1441. {
  1442. $filename = $this->getFileName($key);
  1443. $dirname = dirname($filename);
  1444. if (!file_exists($dirname)) {
  1445. if (!mkdir($dirname, 0755, true)) {
  1446. return false;
  1447. }
  1448. }
  1449. $string = $ttl . '|' . $value;
  1450. return $this->filePutContents($filename, $string) !== false;
  1451. }
  1452. private function filePutContents($filename, $string)
  1453. {
  1454. return file_put_contents($filename, $string, LOCK_EX);
  1455. }
  1456. private function fileGetContents($filename)
  1457. {
  1458. $file = fopen($filename, 'rb');
  1459. if ($file === false) {
  1460. return false;
  1461. }
  1462. $lock = flock($file, LOCK_SH);
  1463. if (!$lock) {
  1464. fclose($file);
  1465. return false;
  1466. }
  1467. $string = '';
  1468. while (!feof($file)) {
  1469. $string .= fread($file, 8192);
  1470. }
  1471. flock($file, LOCK_UN);
  1472. fclose($file);
  1473. return $string;
  1474. }
  1475. private function getString($filename): string
  1476. {
  1477. $data = $this->fileGetContents($filename);
  1478. if ($data === false) {
  1479. return '';
  1480. }
  1481. list($ttl, $string) = explode('|', $data, 2);
  1482. if ($ttl > 0 && time() - filemtime($filename) > $ttl) {
  1483. return '';
  1484. }
  1485. return $string;
  1486. }
  1487. public function get(string $key): string
  1488. {
  1489. $filename = $this->getFileName($key);
  1490. if (!file_exists($filename)) {
  1491. return '';
  1492. }
  1493. $string = $this->getString($filename);
  1494. if ($string == null) {
  1495. return '';
  1496. }
  1497. return $string;
  1498. }
  1499. private function clean(string $path, array $segments, int $len, bool $all) /*: void*/
  1500. {
  1501. $entries = scandir($path);
  1502. foreach ($entries as $entry) {
  1503. if ($entry === '.' || $entry === '..') {
  1504. continue;
  1505. }
  1506. $filename = $path . DIRECTORY_SEPARATOR . $entry;
  1507. if (count($segments) == 0) {
  1508. if (strlen($entry) != $len) {
  1509. continue;
  1510. }
  1511. if (is_file($filename)) {
  1512. if ($all || $this->getString($filename) == null) {
  1513. unlink($filename);
  1514. }
  1515. }
  1516. } else {
  1517. if (strlen($entry) != $segments[0]) {
  1518. continue;
  1519. }
  1520. if (is_dir($filename)) {
  1521. $this->clean($filename, array_slice($segments, 1), $len - $segments[0], $all);
  1522. rmdir($filename);
  1523. }
  1524. }
  1525. }
  1526. }
  1527. public function clear(): bool
  1528. {
  1529. if (!file_exists($this->path) || !is_dir($this->path)) {
  1530. return false;
  1531. }
  1532. $this->clean($this->path, array_filter($this->segments), strlen(md5('')), true);
  1533. return true;
  1534. }
  1535. }
  1536. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedColumn.php
  1537. class ReflectedColumn implements \JsonSerializable
  1538. {
  1539. const DEFAULT_LENGTH = 255;
  1540. const DEFAULT_PRECISION = 19;
  1541. const DEFAULT_SCALE = 4;
  1542. private $name;
  1543. private $type;
  1544. private $length;
  1545. private $precision;
  1546. private $scale;
  1547. private $nullable;
  1548. private $pk;
  1549. private $fk;
  1550. public function __construct(string $name, string $type, int $length, int $precision, int $scale, bool $nullable, bool $pk, string $fk)
  1551. {
  1552. $this->name = $name;
  1553. $this->type = $type;
  1554. $this->length = $length;
  1555. $this->precision = $precision;
  1556. $this->scale = $scale;
  1557. $this->nullable = $nullable;
  1558. $this->pk = $pk;
  1559. $this->fk = $fk;
  1560. $this->sanitize();
  1561. }
  1562. public static function fromReflection(GenericReflection $reflection, array $columnResult): ReflectedColumn
  1563. {
  1564. $name = $columnResult['COLUMN_NAME'];
  1565. $length = (int) $columnResult['CHARACTER_MAXIMUM_LENGTH'];
  1566. $type = $reflection->toJdbcType($columnResult['DATA_TYPE'], $length);
  1567. $precision = (int) $columnResult['NUMERIC_PRECISION'];
  1568. $scale = (int) $columnResult['NUMERIC_SCALE'];
  1569. $nullable = in_array(strtoupper($columnResult['IS_NULLABLE']), ['TRUE', 'YES', 'T', 'Y', '1']);
  1570. $pk = false;
  1571. $fk = '';
  1572. return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
  1573. }
  1574. public static function fromJson( /* object */$json): ReflectedColumn
  1575. {
  1576. $name = $json->name;
  1577. $type = $json->type;
  1578. $length = isset($json->length) ? $json->length : 0;
  1579. $precision = isset($json->precision) ? $json->precision : 0;
  1580. $scale = isset($json->scale) ? $json->scale : 0;
  1581. $nullable = isset($json->nullable) ? $json->nullable : false;
  1582. $pk = isset($json->pk) ? $json->pk : false;
  1583. $fk = isset($json->fk) ? $json->fk : '';
  1584. return new ReflectedColumn($name, $type, $length, $precision, $scale, $nullable, $pk, $fk);
  1585. }
  1586. private function sanitize()
  1587. {
  1588. $this->length = $this->hasLength() ? $this->getLength() : 0;
  1589. $this->precision = $this->hasPrecision() ? $this->getPrecision() : 0;
  1590. $this->scale = $this->hasScale() ? $this->getScale() : 0;
  1591. }
  1592. public function getName(): string
  1593. {
  1594. return $this->name;
  1595. }
  1596. public function getNullable(): bool
  1597. {
  1598. return $this->nullable;
  1599. }
  1600. public function getType(): string
  1601. {
  1602. return $this->type;
  1603. }
  1604. public function getLength(): int
  1605. {
  1606. return $this->length ?: self::DEFAULT_LENGTH;
  1607. }
  1608. public function getPrecision(): int
  1609. {
  1610. return $this->precision ?: self::DEFAULT_PRECISION;
  1611. }
  1612. public function getScale(): int
  1613. {
  1614. return $this->scale ?: self::DEFAULT_SCALE;
  1615. }
  1616. public function hasLength(): bool
  1617. {
  1618. return in_array($this->type, ['varchar', 'varbinary']);
  1619. }
  1620. public function hasPrecision(): bool
  1621. {
  1622. return $this->type == 'decimal';
  1623. }
  1624. public function hasScale(): bool
  1625. {
  1626. return $this->type == 'decimal';
  1627. }
  1628. public function isBinary(): bool
  1629. {
  1630. return in_array($this->type, ['blob', 'varbinary']);
  1631. }
  1632. public function isBoolean(): bool
  1633. {
  1634. return $this->type == 'boolean';
  1635. }
  1636. public function isGeometry(): bool
  1637. {
  1638. return $this->type == 'geometry';
  1639. }
  1640. public function isInteger(): bool
  1641. {
  1642. return in_array($this->type, ['integer', 'bigint', 'smallint', 'tinyint']);
  1643. }
  1644. public function setPk($value) /*: void*/
  1645. {
  1646. $this->pk = $value;
  1647. }
  1648. public function getPk(): bool
  1649. {
  1650. return $this->pk;
  1651. }
  1652. public function setFk($value) /*: void*/
  1653. {
  1654. $this->fk = $value;
  1655. }
  1656. public function getFk(): string
  1657. {
  1658. return $this->fk;
  1659. }
  1660. public function serialize()
  1661. {
  1662. return [
  1663. 'name' => $this->name,
  1664. 'type' => $this->type,
  1665. 'length' => $this->length,
  1666. 'precision' => $this->precision,
  1667. 'scale' => $this->scale,
  1668. 'nullable' => $this->nullable,
  1669. 'pk' => $this->pk,
  1670. 'fk' => $this->fk,
  1671. ];
  1672. }
  1673. public function jsonSerialize()
  1674. {
  1675. return array_filter($this->serialize());
  1676. }
  1677. }
  1678. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedDatabase.php
  1679. class ReflectedDatabase implements \JsonSerializable
  1680. {
  1681. private $tableTypes;
  1682. public function __construct(array $tableTypes)
  1683. {
  1684. $this->tableTypes = $tableTypes;
  1685. }
  1686. public static function fromReflection(GenericReflection $reflection): ReflectedDatabase
  1687. {
  1688. $tableTypes = [];
  1689. foreach ($reflection->getTables() as $table) {
  1690. $tableName = $table['TABLE_NAME'];
  1691. $tableType = $table['TABLE_TYPE'];
  1692. if (in_array($tableName, $reflection->getIgnoredTables())) {
  1693. continue;
  1694. }
  1695. $tableTypes[$tableName] = $tableType;
  1696. }
  1697. return new ReflectedDatabase($tableTypes);
  1698. }
  1699. public static function fromJson( /* object */$json): ReflectedDatabase
  1700. {
  1701. $tableTypes = (array) $json->tables;
  1702. return new ReflectedDatabase($tableTypes);
  1703. }
  1704. public function hasTable(string $tableName): bool
  1705. {
  1706. return isset($this->tableTypes[$tableName]);
  1707. }
  1708. public function getType(string $tableName): string
  1709. {
  1710. return isset($this->tableTypes[$tableName]) ? $this->tableTypes[$tableName] : '';
  1711. }
  1712. public function getTableNames(): array
  1713. {
  1714. return array_keys($this->tableTypes);
  1715. }
  1716. public function removeTable(string $tableName): bool
  1717. {
  1718. if (!isset($this->tableTypes[$tableName])) {
  1719. return false;
  1720. }
  1721. unset($this->tableTypes[$tableName]);
  1722. return true;
  1723. }
  1724. public function serialize()
  1725. {
  1726. return [
  1727. 'tables' => $this->tableTypes,
  1728. ];
  1729. }
  1730. public function jsonSerialize()
  1731. {
  1732. return $this->serialize();
  1733. }
  1734. }
  1735. // file: src/Tqdev/PhpCrudApi/Column/Reflection/ReflectedTable.php
  1736. class ReflectedTable implements \JsonSerializable
  1737. {
  1738. private $name;
  1739. private $type;
  1740. private $columns;
  1741. private $pk;
  1742. private $fks;
  1743. public function __construct(string $name, string $type, array $columns)
  1744. {
  1745. $this->name = $name;
  1746. $this->type = $type;
  1747. $this->columns = [];
  1748. foreach ($columns as $column) {
  1749. $columnName = $column->getName();
  1750. $this->columns[$columnName] = $column;
  1751. }
  1752. $this->pk = null;
  1753. foreach ($columns as $column) {
  1754. if ($column->getPk() == true) {
  1755. $this->pk = $column;
  1756. }
  1757. }
  1758. $this->fks = [];
  1759. foreach ($columns as $column) {
  1760. $columnName = $column->getName();
  1761. $referencedTableName = $column->getFk();
  1762. if ($referencedTableName != '') {
  1763. $this->fks[$columnName] = $referencedTableName;
  1764. }
  1765. }
  1766. }
  1767. public static function fromReflection(GenericReflection $reflection, string $name, string $type): ReflectedTable
  1768. {
  1769. $columns = [];
  1770. foreach ($reflection->getTableColumns($name, $type) as $tableColumn) {
  1771. $column = ReflectedColumn::fromReflection($reflection, $tableColumn);
  1772. $columns[$column->getName()] = $column;
  1773. }
  1774. $columnNames = $reflection->getTablePrimaryKeys($name);
  1775. if (count($columnNames) == 1) {
  1776. $columnName = $columnNames[0];
  1777. if (isset($columns[$columnName])) {
  1778. $pk = $columns[$columnName];
  1779. $pk->setPk(true);
  1780. }
  1781. }
  1782. $fks = $reflection->getTableForeignKeys($name);
  1783. foreach ($fks as $columnName => $table) {
  1784. $columns[$columnName]->setFk($table);
  1785. }
  1786. return new ReflectedTable($name, $type, array_values($columns));
  1787. }
  1788. public static function fromJson( /* object */$json): ReflectedTable
  1789. {
  1790. $name = $json->name;
  1791. $type = $json->type;
  1792. $columns = [];
  1793. if (isset($json->columns) && is_array($json->columns)) {
  1794. foreach ($json->columns as $column) {
  1795. $columns[] = ReflectedColumn::fromJson($column);
  1796. }
  1797. }
  1798. return new ReflectedTable($name, $type, $columns);
  1799. }
  1800. public function hasColumn(string $columnName): bool
  1801. {
  1802. return isset($this->columns[$columnName]);
  1803. }
  1804. public function hasPk(): bool
  1805. {
  1806. return $this->pk != null;
  1807. }
  1808. public function getPk() /*: ?ReflectedColumn */
  1809. {
  1810. return $this->pk;
  1811. }
  1812. public function getName(): string
  1813. {
  1814. return $this->name;
  1815. }
  1816. public function getType(): string
  1817. {
  1818. return $this->type;
  1819. }
  1820. public function getColumnNames(): array
  1821. {
  1822. return array_keys($this->columns);
  1823. }
  1824. public function getColumn($columnName): ReflectedColumn
  1825. {
  1826. return $this->columns[$columnName];
  1827. }
  1828. public function getFksTo(string $tableName): array
  1829. {
  1830. $columns = array();
  1831. foreach ($this->fks as $columnName => $referencedTableName) {
  1832. if ($tableName == $referencedTableName) {
  1833. $columns[] = $this->columns[$columnName];
  1834. }
  1835. }
  1836. return $columns;
  1837. }
  1838. public function removeColumn(string $columnName): bool
  1839. {
  1840. if (!isset($this->columns[$columnName])) {
  1841. return false;
  1842. }
  1843. unset($this->columns[$columnName]);
  1844. return true;
  1845. }
  1846. public function serialize()
  1847. {
  1848. return [
  1849. 'name' => $this->name,
  1850. 'type' => $this->type,
  1851. 'columns' => array_values($this->columns),
  1852. ];
  1853. }
  1854. public function jsonSerialize()
  1855. {
  1856. return $this->serialize();
  1857. }
  1858. }
  1859. // file: src/Tqdev/PhpCrudApi/Column/DefinitionService.php
  1860. class DefinitionService
  1861. {
  1862. private $db;
  1863. private $reflection;
  1864. public function __construct(GenericDB $db, ReflectionService $reflection)
  1865. {
  1866. $this->db = $db;
  1867. $this->reflection = $reflection;
  1868. }
  1869. public function updateTable(string $tableName, /* object */ $changes): bool
  1870. {
  1871. $table = $this->reflection->getTable($tableName);
  1872. $newTable = ReflectedTable::fromJson((object) array_merge((array) $table->jsonSerialize(), (array) $changes));
  1873. if ($table->getName() != $newTable->getName()) {
  1874. if (!$this->db->definition()->renameTable($table->getName(), $newTable->getName())) {
  1875. return false;
  1876. }
  1877. }
  1878. return true;
  1879. }
  1880. public function updateColumn(string $tableName, string $columnName, /* object */ $changes): bool
  1881. {
  1882. $table = $this->reflection->getTable($tableName);
  1883. $column = $table->getColumn($columnName);
  1884. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  1885. if ($newColumn->getPk() != $column->getPk() && $table->hasPk()) {
  1886. $oldColumn = $table->getPk();
  1887. if ($oldColumn->getName() != $columnName) {
  1888. $oldColumn->setPk(false);
  1889. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $oldColumn->getName(), $oldColumn)) {
  1890. return false;
  1891. }
  1892. }
  1893. }
  1894. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), ['pk' => false, 'fk' => false]));
  1895. if ($newColumn->getPk() != $column->getPk() && !$newColumn->getPk()) {
  1896. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $column->getName(), $newColumn)) {
  1897. return false;
  1898. }
  1899. }
  1900. if ($newColumn->getFk() != $column->getFk() && !$newColumn->getFk()) {
  1901. if (!$this->db->definition()->removeColumnForeignKey($table->getName(), $column->getName(), $newColumn)) {
  1902. return false;
  1903. }
  1904. }
  1905. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  1906. $newColumn->setPk(false);
  1907. $newColumn->setFk('');
  1908. if ($newColumn->getName() != $column->getName()) {
  1909. if (!$this->db->definition()->renameColumn($table->getName(), $column->getName(), $newColumn)) {
  1910. return false;
  1911. }
  1912. }
  1913. if ($newColumn->getType() != $column->getType() ||
  1914. $newColumn->getLength() != $column->getLength() ||
  1915. $newColumn->getPrecision() != $column->getPrecision() ||
  1916. $newColumn->getScale() != $column->getScale()
  1917. ) {
  1918. if (!$this->db->definition()->retypeColumn($table->getName(), $newColumn->getName(), $newColumn)) {
  1919. return false;
  1920. }
  1921. }
  1922. if ($newColumn->getNullable() != $column->getNullable()) {
  1923. if (!$this->db->definition()->setColumnNullable($table->getName(), $newColumn->getName(), $newColumn)) {
  1924. return false;
  1925. }
  1926. }
  1927. $newColumn = ReflectedColumn::fromJson((object) array_merge((array) $column->jsonSerialize(), (array) $changes));
  1928. if ($newColumn->getFk()) {
  1929. if (!$this->db->definition()->addColumnForeignKey($table->getName(), $newColumn->getName(), $newColumn)) {
  1930. return false;
  1931. }
  1932. }
  1933. if ($newColumn->getPk()) {
  1934. if (!$this->db->definition()->addColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
  1935. return false;
  1936. }
  1937. }
  1938. return true;
  1939. }
  1940. public function addTable( /* object */$definition)
  1941. {
  1942. $newTable = ReflectedTable::fromJson($definition);
  1943. if (!$this->db->definition()->addTable($newTable)) {
  1944. return false;
  1945. }
  1946. return true;
  1947. }
  1948. public function addColumn(string $tableName, /* object */ $definition)
  1949. {
  1950. $newColumn = ReflectedColumn::fromJson($definition);
  1951. if (!$this->db->definition()->addColumn($tableName, $newColumn)) {
  1952. return false;
  1953. }
  1954. if ($newColumn->getFk()) {
  1955. if (!$this->db->definition()->addColumnForeignKey($tableName, $newColumn->getName(), $newColumn)) {
  1956. return false;
  1957. }
  1958. }
  1959. if ($newColumn->getPk()) {
  1960. if (!$this->db->definition()->addColumnPrimaryKey($tableName, $newColumn->getName(), $newColumn)) {
  1961. return false;
  1962. }
  1963. }
  1964. return true;
  1965. }
  1966. public function removeTable(string $tableName)
  1967. {
  1968. if (!$this->db->definition()->removeTable($tableName)) {
  1969. return false;
  1970. }
  1971. return true;
  1972. }
  1973. public function removeColumn(string $tableName, string $columnName)
  1974. {
  1975. $table = $this->reflection->getTable($tableName);
  1976. $newColumn = $table->getColumn($columnName);
  1977. if ($newColumn->getPk()) {
  1978. $newColumn->setPk(false);
  1979. if (!$this->db->definition()->removeColumnPrimaryKey($table->getName(), $newColumn->getName(), $newColumn)) {
  1980. return false;
  1981. }
  1982. }
  1983. if ($newColumn->getFk()) {
  1984. $newColumn->setFk("");
  1985. if (!$this->db->definition()->removeColumnForeignKey($tableName, $columnName, $newColumn)) {
  1986. return false;
  1987. }
  1988. }
  1989. if (!$this->db->definition()->removeColumn($tableName, $columnName)) {
  1990. return false;
  1991. }
  1992. return true;
  1993. }
  1994. }
  1995. // file: src/Tqdev/PhpCrudApi/Column/ReflectionService.php
  1996. class ReflectionService
  1997. {
  1998. private $db;
  1999. private $cache;
  2000. private $ttl;
  2001. private $database;
  2002. private $tables;
  2003. public function __construct(GenericDB $db, Cache $cache, int $ttl)
  2004. {
  2005. $this->db = $db;
  2006. $this->cache = $cache;
  2007. $this->ttl = $ttl;
  2008. $this->database = $this->loadDatabase(true);
  2009. $this->tables = [];
  2010. }
  2011. private function loadDatabase(bool $useCache): ReflectedDatabase
  2012. {
  2013. $data = $useCache ? $this->cache->get('ReflectedDatabase') : '';
  2014. if ($data != '') {
  2015. $database = ReflectedDatabase::fromJson(json_decode(gzuncompress($data)));
  2016. } else {
  2017. $database = ReflectedDatabase::fromReflection($this->db->reflection());
  2018. $data = gzcompress(json_encode($database, JSON_UNESCAPED_UNICODE));
  2019. $this->cache->set('ReflectedDatabase', $data, $this->ttl);
  2020. }
  2021. return $database;
  2022. }
  2023. private function loadTable(string $tableName, bool $useCache): ReflectedTable
  2024. {
  2025. $data = $useCache ? $this->cache->get("ReflectedTable($tableName)") : '';
  2026. if ($data != '') {
  2027. $table = ReflectedTable::fromJson(json_decode(gzuncompress($data)));
  2028. } else {
  2029. $tableType = $this->database->getType($tableName);
  2030. $table = ReflectedTable::fromReflection($this->db->reflection(), $tableName, $tableType);
  2031. $data = gzcompress(json_encode($table, JSON_UNESCAPED_UNICODE));
  2032. $this->cache->set("ReflectedTable($tableName)", $data, $this->ttl);
  2033. }
  2034. return $table;
  2035. }
  2036. public function refreshTables()
  2037. {
  2038. $this->database = $this->loadDatabase(false);
  2039. }
  2040. public function refreshTable(string $tableName)
  2041. {
  2042. $this->tables[$tableName] = $this->loadTable($tableName, false);
  2043. }
  2044. public function hasTable(string $tableName): bool
  2045. {
  2046. return $this->database->hasTable($tableName);
  2047. }
  2048. public function getType(string $tableName): string
  2049. {
  2050. return $this->database->getType($tableName);
  2051. }
  2052. public function getTable(string $tableName): ReflectedTable
  2053. {
  2054. if (!isset($this->tables[$tableName])) {
  2055. $this->tables[$tableName] = $this->loadTable($tableName, true);
  2056. }
  2057. return $this->tables[$tableName];
  2058. }
  2059. public function getTableNames(): array
  2060. {
  2061. return $this->database->getTableNames();
  2062. }
  2063. public function getDatabaseName(): string
  2064. {
  2065. return $this->database->getName();
  2066. }
  2067. public function removeTable(string $tableName): bool
  2068. {
  2069. unset($this->tables[$tableName]);
  2070. return $this->database->removeTable($tableName);
  2071. }
  2072. }
  2073. // file: src/Tqdev/PhpCrudApi/Controller/CacheController.php
  2074. class CacheController
  2075. {
  2076. private $cache;
  2077. private $responder;
  2078. public function __construct(Router $router, Responder $responder, Cache $cache)
  2079. {
  2080. $router->register('GET', '/cache/clear', array($this, 'clear'));
  2081. $this->cache = $cache;
  2082. $this->responder = $responder;
  2083. }
  2084. public function clear(ServerRequestInterface $request): ResponseInterface
  2085. {
  2086. return $this->responder->success($this->cache->clear());
  2087. }
  2088. }
  2089. // file: src/Tqdev/PhpCrudApi/Controller/ColumnController.php
  2090. class ColumnController
  2091. {
  2092. private $responder;
  2093. private $reflection;
  2094. private $definition;
  2095. public function __construct(Router $router, Responder $responder, ReflectionService $reflection, DefinitionService $definition)
  2096. {
  2097. $router->register('GET', '/columns', array($this, 'getDatabase'));
  2098. $router->register('GET', '/columns/*', array($this, 'getTable'));
  2099. $router->register('GET', '/columns/*/*', array($this, 'getColumn'));
  2100. $router->register('PUT', '/columns/*', array($this, 'updateTable'));
  2101. $router->register('PUT', '/columns/*/*', array($this, 'updateColumn'));
  2102. $router->register('POST', '/columns', array($this, 'addTable'));
  2103. $router->register('POST', '/columns/*', array($this, 'addColumn'));
  2104. $router->register('DELETE', '/columns/*', array($this, 'removeTable'));
  2105. $router->register('DELETE', '/columns/*/*', array($this, 'removeColumn'));
  2106. $this->responder = $responder;
  2107. $this->reflection = $reflection;
  2108. $this->definition = $definition;
  2109. }
  2110. public function getDatabase(ServerRequestInterface $request): ResponseInterface
  2111. {
  2112. $tables = [];
  2113. foreach ($this->reflection->getTableNames() as $table) {
  2114. $tables[] = $this->reflection->getTable($table);
  2115. }
  2116. $database = ['tables' => $tables];
  2117. return $this->responder->success($database);
  2118. }
  2119. public function getTable(ServerRequestInterface $request): ResponseInterface
  2120. {
  2121. $tableName = RequestUtils::getPathSegment($request, 2);
  2122. if (!$this->reflection->hasTable($tableName)) {
  2123. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2124. }
  2125. $table = $this->reflection->getTable($tableName);
  2126. return $this->responder->success($table);
  2127. }
  2128. public function getColumn(ServerRequestInterface $request): ResponseInterface
  2129. {
  2130. $tableName = RequestUtils::getPathSegment($request, 2);
  2131. $columnName = RequestUtils::getPathSegment($request, 3);
  2132. if (!$this->reflection->hasTable($tableName)) {
  2133. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2134. }
  2135. $table = $this->reflection->getTable($tableName);
  2136. if (!$table->hasColumn($columnName)) {
  2137. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  2138. }
  2139. $column = $table->getColumn($columnName);
  2140. return $this->responder->success($column);
  2141. }
  2142. public function updateTable(ServerRequestInterface $request): ResponseInterface
  2143. {
  2144. $tableName = RequestUtils::getPathSegment($request, 2);
  2145. if (!$this->reflection->hasTable($tableName)) {
  2146. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2147. }
  2148. $success = $this->definition->updateTable($tableName, $request->getParsedBody());
  2149. if ($success) {
  2150. $this->reflection->refreshTables();
  2151. }
  2152. return $this->responder->success($success);
  2153. }
  2154. public function updateColumn(ServerRequestInterface $request): ResponseInterface
  2155. {
  2156. $tableName = RequestUtils::getPathSegment($request, 2);
  2157. $columnName = RequestUtils::getPathSegment($request, 3);
  2158. if (!$this->reflection->hasTable($tableName)) {
  2159. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2160. }
  2161. $table = $this->reflection->getTable($tableName);
  2162. if (!$table->hasColumn($columnName)) {
  2163. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  2164. }
  2165. $success = $this->definition->updateColumn($tableName, $columnName, $request->getParsedBody());
  2166. if ($success) {
  2167. $this->reflection->refreshTable($tableName);
  2168. }
  2169. return $this->responder->success($success);
  2170. }
  2171. public function addTable(ServerRequestInterface $request): ResponseInterface
  2172. {
  2173. $tableName = $request->getParsedBody()->name;
  2174. if ($this->reflection->hasTable($tableName)) {
  2175. return $this->responder->error(ErrorCode::TABLE_ALREADY_EXISTS, $tableName);
  2176. }
  2177. $success = $this->definition->addTable($request->getParsedBody());
  2178. if ($success) {
  2179. $this->reflection->refreshTables();
  2180. }
  2181. return $this->responder->success($success);
  2182. }
  2183. public function addColumn(ServerRequestInterface $request): ResponseInterface
  2184. {
  2185. $tableName = RequestUtils::getPathSegment($request, 2);
  2186. if (!$this->reflection->hasTable($tableName)) {
  2187. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2188. }
  2189. $columnName = $request->getParsedBody()->name;
  2190. $table = $this->reflection->getTable($tableName);
  2191. if ($table->hasColumn($columnName)) {
  2192. return $this->responder->error(ErrorCode::COLUMN_ALREADY_EXISTS, $columnName);
  2193. }
  2194. $success = $this->definition->addColumn($tableName, $request->getParsedBody());
  2195. if ($success) {
  2196. $this->reflection->refreshTable($tableName);
  2197. }
  2198. return $this->responder->success($success);
  2199. }
  2200. public function removeTable(ServerRequestInterface $request): ResponseInterface
  2201. {
  2202. $tableName = RequestUtils::getPathSegment($request, 2);
  2203. if (!$this->reflection->hasTable($tableName)) {
  2204. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2205. }
  2206. $success = $this->definition->removeTable($tableName);
  2207. if ($success) {
  2208. $this->reflection->refreshTables();
  2209. }
  2210. return $this->responder->success($success);
  2211. }
  2212. public function removeColumn(ServerRequestInterface $request): ResponseInterface
  2213. {
  2214. $tableName = RequestUtils::getPathSegment($request, 2);
  2215. $columnName = RequestUtils::getPathSegment($request, 3);
  2216. if (!$this->reflection->hasTable($tableName)) {
  2217. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $tableName);
  2218. }
  2219. $table = $this->reflection->getTable($tableName);
  2220. if (!$table->hasColumn($columnName)) {
  2221. return $this->responder->error(ErrorCode::COLUMN_NOT_FOUND, $columnName);
  2222. }
  2223. $success = $this->definition->removeColumn($tableName, $columnName);
  2224. if ($success) {
  2225. $this->reflection->refreshTable($tableName);
  2226. }
  2227. return $this->responder->success($success);
  2228. }
  2229. }
  2230. // file: src/Tqdev/PhpCrudApi/Controller/OpenApiController.php
  2231. class OpenApiController
  2232. {
  2233. private $openApi;
  2234. private $responder;
  2235. public function __construct(Router $router, Responder $responder, OpenApiService $openApi)
  2236. {
  2237. $router->register('GET', '/openapi', array($this, 'openapi'));
  2238. $this->openApi = $openApi;
  2239. $this->responder = $responder;
  2240. }
  2241. public function openapi(ServerRequestInterface $request): ResponseInterface
  2242. {
  2243. return $this->responder->success($this->openApi->get());
  2244. }
  2245. }
  2246. // file: src/Tqdev/PhpCrudApi/Controller/RecordController.php
  2247. class RecordController
  2248. {
  2249. private $service;
  2250. private $responder;
  2251. public function __construct(Router $router, Responder $responder, RecordService $service)
  2252. {
  2253. $router->register('GET', '/records/*', array($this, '_list'));
  2254. $router->register('POST', '/records/*', array($this, 'create'));
  2255. $router->register('GET', '/records/*/*', array($this, 'read'));
  2256. $router->register('PUT', '/records/*/*', array($this, 'update'));
  2257. $router->register('DELETE', '/records/*/*', array($this, 'delete'));
  2258. $router->register('PATCH', '/records/*/*', array($this, 'increment'));
  2259. $this->service = $service;
  2260. $this->responder = $responder;
  2261. }
  2262. public function _list(ServerRequestInterface $request): ResponseInterface
  2263. {
  2264. $table = RequestUtils::getPathSegment($request, 2);
  2265. $params = RequestUtils::getParams($request);
  2266. if (!$this->service->hasTable($table)) {
  2267. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2268. }
  2269. return $this->responder->success($this->service->_list($table, $params));
  2270. }
  2271. public function read(ServerRequestInterface $request): ResponseInterface
  2272. {
  2273. $table = RequestUtils::getPathSegment($request, 2);
  2274. if (!$this->service->hasTable($table)) {
  2275. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2276. }
  2277. if ($this->service->getType($table) != 'table') {
  2278. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2279. }
  2280. $id = RequestUtils::getPathSegment($request, 3);
  2281. $params = RequestUtils::getParams($request);
  2282. if (strpos($id, ',') !== false) {
  2283. $ids = explode(',', $id);
  2284. $result = [];
  2285. for ($i = 0; $i < count($ids); $i++) {
  2286. array_push($result, $this->service->read($table, $ids[$i], $params));
  2287. }
  2288. return $this->responder->success($result);
  2289. } else {
  2290. $response = $this->service->read($table, $id, $params);
  2291. if ($response === null) {
  2292. return $this->responder->error(ErrorCode::RECORD_NOT_FOUND, $id);
  2293. }
  2294. return $this->responder->success($response);
  2295. }
  2296. }
  2297. public function create(ServerRequestInterface $request): ResponseInterface
  2298. {
  2299. $table = RequestUtils::getPathSegment($request, 2);
  2300. if (!$this->service->hasTable($table)) {
  2301. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2302. }
  2303. if ($this->service->getType($table) != 'table') {
  2304. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2305. }
  2306. $record = $request->getParsedBody();
  2307. if ($record === null) {
  2308. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  2309. }
  2310. $params = RequestUtils::getParams($request);
  2311. if (is_array($record)) {
  2312. $result = array();
  2313. foreach ($record as $r) {
  2314. $result[] = $this->service->create($table, $r, $params);
  2315. }
  2316. return $this->responder->success($result);
  2317. } else {
  2318. return $this->responder->success($this->service->create($table, $record, $params));
  2319. }
  2320. }
  2321. public function update(ServerRequestInterface $request): ResponseInterface
  2322. {
  2323. $table = RequestUtils::getPathSegment($request, 2);
  2324. if (!$this->service->hasTable($table)) {
  2325. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2326. }
  2327. if ($this->service->getType($table) != 'table') {
  2328. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2329. }
  2330. $id = RequestUtils::getPathSegment($request, 3);
  2331. $params = RequestUtils::getParams($request);
  2332. $record = $request->getParsedBody();
  2333. if ($record === null) {
  2334. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  2335. }
  2336. $ids = explode(',', $id);
  2337. if (is_array($record)) {
  2338. if (count($ids) != count($record)) {
  2339. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2340. }
  2341. $result = array();
  2342. for ($i = 0; $i < count($ids); $i++) {
  2343. $result[] = $this->service->update($table, $ids[$i], $record[$i], $params);
  2344. }
  2345. return $this->responder->success($result);
  2346. } else {
  2347. if (count($ids) != 1) {
  2348. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2349. }
  2350. return $this->responder->success($this->service->update($table, $id, $record, $params));
  2351. }
  2352. }
  2353. public function delete(ServerRequestInterface $request): ResponseInterface
  2354. {
  2355. $table = RequestUtils::getPathSegment($request, 2);
  2356. if (!$this->service->hasTable($table)) {
  2357. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2358. }
  2359. if ($this->service->getType($table) != 'table') {
  2360. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2361. }
  2362. $id = RequestUtils::getPathSegment($request, 3);
  2363. $params = RequestUtils::getParams($request);
  2364. $ids = explode(',', $id);
  2365. if (count($ids) > 1) {
  2366. $result = array();
  2367. for ($i = 0; $i < count($ids); $i++) {
  2368. $result[] = $this->service->delete($table, $ids[$i], $params);
  2369. }
  2370. return $this->responder->success($result);
  2371. } else {
  2372. return $this->responder->success($this->service->delete($table, $id, $params));
  2373. }
  2374. }
  2375. public function increment(ServerRequestInterface $request): ResponseInterface
  2376. {
  2377. $table = RequestUtils::getPathSegment($request, 2);
  2378. if (!$this->service->hasTable($table)) {
  2379. return $this->responder->error(ErrorCode::TABLE_NOT_FOUND, $table);
  2380. }
  2381. if ($this->service->getType($table) != 'table') {
  2382. return $this->responder->error(ErrorCode::OPERATION_NOT_SUPPORTED, __FUNCTION__);
  2383. }
  2384. $id = RequestUtils::getPathSegment($request, 3);
  2385. $record = $request->getParsedBody();
  2386. if ($record === null) {
  2387. return $this->responder->error(ErrorCode::HTTP_MESSAGE_NOT_READABLE, '');
  2388. }
  2389. $params = RequestUtils::getParams($request);
  2390. $ids = explode(',', $id);
  2391. if (is_array($record)) {
  2392. if (count($ids) != count($record)) {
  2393. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2394. }
  2395. $result = array();
  2396. for ($i = 0; $i < count($ids); $i++) {
  2397. $result[] = $this->service->increment($table, $ids[$i], $record[$i], $params);
  2398. }
  2399. return $this->responder->success($result);
  2400. } else {
  2401. if (count($ids) != 1) {
  2402. return $this->responder->error(ErrorCode::ARGUMENT_COUNT_MISMATCH, $id);
  2403. }
  2404. return $this->responder->success($this->service->increment($table, $id, $record, $params));
  2405. }
  2406. }
  2407. }
  2408. // file: src/Tqdev/PhpCrudApi/Controller/Responder.php
  2409. class Responder
  2410. {
  2411. public function error(int $error, string $argument, $details = null): ResponseInterface
  2412. {
  2413. $errorCode = new ErrorCode($error);
  2414. $status = $errorCode->getStatus();
  2415. $document = new ErrorDocument($errorCode, $argument, $details);
  2416. return ResponseFactory::fromObject($status, $document);
  2417. }
  2418. public function success($result): ResponseInterface
  2419. {
  2420. return ResponseFactory::fromObject(ResponseFactory::OK, $result);
  2421. }
  2422. }
  2423. // file: src/Tqdev/PhpCrudApi/Database/ColumnConverter.php
  2424. class ColumnConverter
  2425. {
  2426. private $driver;
  2427. public function __construct(string $driver)
  2428. {
  2429. $this->driver = $driver;
  2430. }
  2431. public function convertColumnValue(ReflectedColumn $column): string
  2432. {
  2433. if ($column->isBinary()) {
  2434. switch ($this->driver) {
  2435. case 'mysql':
  2436. return "FROM_BASE64(?)";
  2437. case 'pgsql':
  2438. return "decode(?, 'base64')";
  2439. case 'sqlsrv':
  2440. return "CONVERT(XML, ?).value('.','varbinary(max)')";
  2441. }
  2442. }
  2443. if ($column->isGeometry()) {
  2444. switch ($this->driver) {
  2445. case 'mysql':
  2446. case 'pgsql':
  2447. return "ST_GeomFromText(?)";
  2448. case 'sqlsrv':
  2449. return "geometry::STGeomFromText(?,0)";
  2450. }
  2451. }
  2452. return '?';
  2453. }
  2454. public function convertColumnName(ReflectedColumn $column, $value): string
  2455. {
  2456. if ($column->isBinary()) {
  2457. switch ($this->driver) {
  2458. case 'mysql':
  2459. return "TO_BASE64($value) as $value";
  2460. case 'pgsql':
  2461. return "encode($value::bytea, 'base64') as $value";
  2462. case 'sqlsrv':
  2463. return "CASE WHEN $value IS NULL THEN NULL ELSE (SELECT CAST($value as varbinary(max)) FOR XML PATH(''), BINARY BASE64) END as $value";
  2464. }
  2465. }
  2466. if ($column->isGeometry()) {
  2467. switch ($this->driver) {
  2468. case 'mysql':
  2469. case 'pgsql':
  2470. return "ST_AsText($value) as $value";
  2471. case 'sqlsrv':
  2472. return "REPLACE($value.STAsText(),' (','(') as $value";
  2473. }
  2474. }
  2475. return $value;
  2476. }
  2477. }
  2478. // file: src/Tqdev/PhpCrudApi/Database/ColumnsBuilder.php
  2479. class ColumnsBuilder
  2480. {
  2481. private $driver;
  2482. private $converter;
  2483. public function __construct(string $driver)
  2484. {
  2485. $this->driver = $driver;
  2486. $this->converter = new ColumnConverter($driver);
  2487. }
  2488. public function getOffsetLimit(int $offset, int $limit): string
  2489. {
  2490. if ($limit < 0 || $offset < 0) {
  2491. return '';
  2492. }
  2493. switch ($this->driver) {
  2494. case 'mysql':return " LIMIT $offset, $limit";
  2495. case 'pgsql':return " LIMIT $limit OFFSET $offset";
  2496. case 'sqlsrv':return " OFFSET $offset ROWS FETCH NEXT $limit ROWS ONLY";
  2497. }
  2498. }
  2499. private function quoteColumnName(ReflectedColumn $column): string
  2500. {
  2501. return '"' . $column->getName() . '"';
  2502. }
  2503. public function getOrderBy(ReflectedTable $table, array $columnOrdering): string
  2504. {
  2505. if (count($columnOrdering) == 0) {
  2506. return '';
  2507. }
  2508. $results = array();
  2509. foreach ($columnOrdering as $i => list($columnName, $ordering)) {
  2510. $column = $table->getColumn($columnName);
  2511. $quotedColumnName = $this->quoteColumnName($column);
  2512. $results[] = $quotedColumnName . ' ' . $ordering;
  2513. }
  2514. return ' ORDER BY ' . implode(',', $results);
  2515. }
  2516. public function getSelect(ReflectedTable $table, array $columnNames): string
  2517. {
  2518. $results = array();
  2519. foreach ($columnNames as $columnName) {
  2520. $column = $table->getColumn($columnName);
  2521. $quotedColumnName = $this->quoteColumnName($column);
  2522. $quotedColumnName = $this->converter->convertColumnName($column, $quotedColumnName);
  2523. $results[] = $quotedColumnName;
  2524. }
  2525. return implode(',', $results);
  2526. }
  2527. public function getInsert(ReflectedTable $table, array $columnValues): string
  2528. {
  2529. $columns = array();
  2530. $values = array();
  2531. foreach ($columnValues as $columnName => $columnValue) {
  2532. $column = $table->getColumn($columnName);
  2533. $quotedColumnName = $this->quoteColumnName($column);
  2534. $columns[] = $quotedColumnName;
  2535. $columnValue = $this->converter->convertColumnValue($column);
  2536. $values[] = $columnValue;
  2537. }
  2538. $columnsSql = '(' . implode(',', $columns) . ')';
  2539. $valuesSql = '(' . implode(',', $values) . ')';
  2540. $outputColumn = $this->quoteColumnName($table->getPk());
  2541. switch ($this->driver) {
  2542. case 'mysql':return "$columnsSql VALUES $valuesSql";
  2543. case 'pgsql':return "$columnsSql VALUES $valuesSql RETURNING $outputColumn";
  2544. case 'sqlsrv':return "$columnsSql OUTPUT INSERTED.$outputColumn VALUES $valuesSql";
  2545. }
  2546. }
  2547. public function getUpdate(ReflectedTable $table, array $columnValues): string
  2548. {
  2549. $results = array();
  2550. foreach ($columnValues as $columnName => $columnValue) {
  2551. $column = $table->getColumn($columnName);
  2552. $quotedColumnName = $this->quoteColumnName($column);
  2553. $columnValue = $this->converter->convertColumnValue($column);
  2554. $results[] = $quotedColumnName . '=' . $columnValue;
  2555. }
  2556. return implode(',', $results);
  2557. }
  2558. public function getIncrement(ReflectedTable $table, array $columnValues): string
  2559. {
  2560. $results = array();
  2561. foreach ($columnValues as $columnName => $columnValue) {
  2562. if (!is_numeric($columnValue)) {
  2563. continue;
  2564. }
  2565. $column = $table->getColumn($columnName);
  2566. $quotedColumnName = $this->quoteColumnName($column);
  2567. $columnValue = $this->converter->convertColumnValue($column);
  2568. $results[] = $quotedColumnName . '=' . $quotedColumnName . '+' . $columnValue;
  2569. }
  2570. return implode(',', $results);
  2571. }
  2572. }
  2573. // file: src/Tqdev/PhpCrudApi/Database/ConditionsBuilder.php
  2574. class ConditionsBuilder
  2575. {
  2576. private $driver;
  2577. public function __construct(string $driver)
  2578. {
  2579. $this->driver = $driver;
  2580. }
  2581. private function getConditionSql(Condition $condition, array &$arguments): string
  2582. {
  2583. if ($condition instanceof AndCondition) {
  2584. return $this->getAndConditionSql($condition, $arguments);
  2585. }
  2586. if ($condition instanceof OrCondition) {
  2587. return $this->getOrConditionSql($condition, $arguments);
  2588. }
  2589. if ($condition instanceof NotCondition) {
  2590. return $this->getNotConditionSql($condition, $arguments);
  2591. }
  2592. if ($condition instanceof SpatialCondition) {
  2593. return $this->getSpatialConditionSql($condition, $arguments);
  2594. }
  2595. if ($condition instanceof ColumnCondition) {
  2596. return $this->getColumnConditionSql($condition, $arguments);
  2597. }
  2598. throw new \Exception('Unknown Condition: ' . get_class($condition));
  2599. }
  2600. private function getAndConditionSql(AndCondition $and, array &$arguments): string
  2601. {
  2602. $parts = [];
  2603. foreach ($and->getConditions() as $condition) {
  2604. $parts[] = $this->getConditionSql($condition, $arguments);
  2605. }
  2606. return '(' . implode(' AND ', $parts) . ')';
  2607. }
  2608. private function getOrConditionSql(OrCondition $or, array &$arguments): string
  2609. {
  2610. $parts = [];
  2611. foreach ($or->getConditions() as $condition) {
  2612. $parts[] = $this->getConditionSql($condition, $arguments);
  2613. }
  2614. return '(' . implode(' OR ', $parts) . ')';
  2615. }
  2616. private function getNotConditionSql(NotCondition $not, array &$arguments): string
  2617. {
  2618. $condition = $not->getCondition();
  2619. return '(NOT ' . $this->getConditionSql($condition, $arguments) . ')';
  2620. }
  2621. private function quoteColumnName(ReflectedColumn $column): string
  2622. {
  2623. return '"' . $column->getName() . '"';
  2624. }
  2625. private function escapeLikeValue(string $value): string
  2626. {
  2627. return addcslashes($value, '%_');
  2628. }
  2629. private function getColumnConditionSql(ColumnCondition $condition, array &$arguments): string
  2630. {
  2631. $column = $this->quoteColumnName($condition->getColumn());
  2632. $operator = $condition->getOperator();
  2633. $value = $condition->getValue();
  2634. switch ($operator) {
  2635. case 'cs':
  2636. $sql = "$column LIKE ?";
  2637. $arguments[] = '%' . $this->escapeLikeValue($value) . '%';
  2638. break;
  2639. case 'sw':
  2640. $sql = "$column LIKE ?";
  2641. $arguments[] = $this->escapeLikeValue($value) . '%';
  2642. break;
  2643. case 'ew':
  2644. $sql = "$column LIKE ?";
  2645. $arguments[] = '%' . $this->escapeLikeValue($value);
  2646. break;
  2647. case 'eq':
  2648. $sql = "$column = ?";
  2649. $arguments[] = $value;
  2650. break;
  2651. case 'lt':
  2652. $sql = "$column < ?";
  2653. $arguments[] = $value;
  2654. break;
  2655. case 'le':
  2656. $sql = "$column <= ?";
  2657. $arguments[] = $value;
  2658. break;
  2659. case 'ge':
  2660. $sql = "$column >= ?";
  2661. $arguments[] = $value;
  2662. break;
  2663. case 'gt':
  2664. $sql = "$column > ?";
  2665. $arguments[] = $value;
  2666. break;
  2667. case 'bt':
  2668. $parts = explode(',', $value, 2);
  2669. $count = count($parts);
  2670. if ($count == 2) {
  2671. $sql = "($column >= ? AND $column <= ?)";
  2672. $arguments[] = $parts[0];
  2673. $arguments[] = $parts[1];
  2674. } else {
  2675. $sql = "FALSE";
  2676. }
  2677. break;
  2678. case 'in':
  2679. $parts = explode(',', $value);
  2680. $count = count($parts);
  2681. if ($count > 0) {
  2682. $qmarks = implode(',', str_split(str_repeat('?', $count)));
  2683. $sql = "$column IN ($qmarks)";
  2684. for ($i = 0; $i < $count; $i++) {
  2685. $arguments[] = $parts[$i];
  2686. }
  2687. } else {
  2688. $sql = "FALSE";
  2689. }
  2690. break;
  2691. case 'is':
  2692. $sql = "$column IS NULL";
  2693. break;
  2694. }
  2695. return $sql;
  2696. }
  2697. private function getSpatialFunctionName(string $operator): string
  2698. {
  2699. switch ($operator) {
  2700. case 'co':return 'ST_Contains';
  2701. case 'cr':return 'ST_Crosses';
  2702. case 'di':return 'ST_Disjoint';
  2703. case 'eq':return 'ST_Equals';
  2704. case 'in':return 'ST_Intersects';
  2705. case 'ov':return 'ST_Overlaps';
  2706. case 'to':return 'ST_Touches';
  2707. case 'wi':return 'ST_Within';
  2708. case 'ic':return 'ST_IsClosed';
  2709. case 'is':return 'ST_IsSimple';
  2710. case 'iv':return 'ST_IsValid';
  2711. }
  2712. }
  2713. private function hasSpatialArgument(string $operator): bool
  2714. {
  2715. return in_array($operator, ['ic', 'is', 'iv']) ? false : true;
  2716. }
  2717. private function getSpatialFunctionCall(string $functionName, string $column, bool $hasArgument): string
  2718. {
  2719. switch ($this->driver) {
  2720. case 'mysql':
  2721. case 'pgsql':
  2722. $argument = $hasArgument ? 'ST_GeomFromText(?)' : '';
  2723. return "$functionName($column, $argument)=TRUE";
  2724. case 'sqlsrv':
  2725. $functionName = str_replace('ST_', 'ST', $functionName);
  2726. $argument = $hasArgument ? 'geometry::STGeomFromText(?,0)' : '';
  2727. return "$column.$functionName($argument)=1";
  2728. }
  2729. }
  2730. private function getSpatialConditionSql(ColumnCondition $condition, array &$arguments): string
  2731. {
  2732. $column = $this->quoteColumnName($condition->getColumn());
  2733. $operator = $condition->getOperator();
  2734. $value = $condition->getValue();
  2735. $functionName = $this->getSpatialFunctionName($operator);
  2736. $hasArgument = $this->hasSpatialArgument($operator);
  2737. $sql = $this->getSpatialFunctionCall($functionName, $column, $hasArgument);
  2738. if ($hasArgument) {
  2739. $arguments[] = $value;
  2740. }
  2741. return $sql;
  2742. }
  2743. public function getWhereClause(Condition $condition, array &$arguments): string
  2744. {
  2745. if ($condition instanceof NoCondition) {
  2746. return '';
  2747. }
  2748. return ' WHERE ' . $this->getConditionSql($condition, $arguments);
  2749. }
  2750. }
  2751. // file: src/Tqdev/PhpCrudApi/Database/DataConverter.php
  2752. class DataConverter
  2753. {
  2754. private $driver;
  2755. public function __construct(string $driver)
  2756. {
  2757. $this->driver = $driver;
  2758. }
  2759. private function convertRecordValue($conversion, $value)
  2760. {
  2761. switch ($conversion) {
  2762. case 'boolean':
  2763. return $value ? true : false;
  2764. case 'integer':
  2765. return (int) $value;
  2766. }
  2767. return $value;
  2768. }
  2769. private function getRecordValueConversion(ReflectedColumn $column): string
  2770. {
  2771. if (in_array($this->driver, ['mysql', 'sqlsrv']) && $column->isBoolean()) {
  2772. return 'boolean';
  2773. }
  2774. if ($this->driver == 'sqlsrv' && $column->getType() == 'bigint') {
  2775. return 'integer';
  2776. }
  2777. return 'none';
  2778. }
  2779. public function convertRecords(ReflectedTable $table, array $columnNames, array &$records) /*: void*/
  2780. {
  2781. foreach ($columnNames as $columnName) {
  2782. $column = $table->getColumn($columnName);
  2783. $conversion = $this->getRecordValueConversion($column);
  2784. if ($conversion != 'none') {
  2785. foreach ($records as $i => $record) {
  2786. $value = $records[$i][$columnName];
  2787. if ($value === null) {
  2788. continue;
  2789. }
  2790. $records[$i][$columnName] = $this->convertRecordValue($conversion, $value);
  2791. }
  2792. }
  2793. }
  2794. }
  2795. private function convertInputValue($conversion, $value)
  2796. {
  2797. switch ($conversion) {
  2798. case 'base64url_to_base64':
  2799. return str_pad(strtr($value, '-_', '+/'), ceil(strlen($value) / 4) * 4, '=', STR_PAD_RIGHT);
  2800. }
  2801. return $value;
  2802. }
  2803. private function getInputValueConversion(ReflectedColumn $column): string
  2804. {
  2805. if ($column->isBinary()) {
  2806. return 'base64url_to_base64';
  2807. }
  2808. return 'none';
  2809. }
  2810. public function convertColumnValues(ReflectedTable $table, array &$columnValues) /*: void*/
  2811. {
  2812. $columnNames = array_keys($columnValues);
  2813. foreach ($columnNames as $columnName) {
  2814. $column = $table->getColumn($columnName);
  2815. $conversion = $this->getInputValueConversion($column);
  2816. if ($conversion != 'none') {
  2817. $value = $columnValues[$columnName];
  2818. if ($value !== null) {
  2819. $columnValues[$columnName] = $this->convertInputValue($conversion, $value);
  2820. }
  2821. }
  2822. }
  2823. }
  2824. }
  2825. // file: src/Tqdev/PhpCrudApi/Database/GenericDB.php
  2826. class GenericDB
  2827. {
  2828. private $driver;
  2829. private $database;
  2830. private $pdo;
  2831. private $reflection;
  2832. private $definition;
  2833. private $conditions;
  2834. private $columns;
  2835. private $converter;
  2836. private function getDsn(string $address, int $port, string $database): string
  2837. {
  2838. switch ($this->driver) {
  2839. case 'mysql':return "$this->driver:host=$address;port=$port;dbname=$database;charset=utf8mb4";
  2840. case 'pgsql':return "$this->driver:host=$address port=$port dbname=$database options='--client_encoding=UTF8'";
  2841. case 'sqlsrv':return "$this->driver:Server=$address,$port;Database=$database";
  2842. }
  2843. }
  2844. private function getCommands(): array
  2845. {
  2846. switch ($this->driver) {
  2847. case 'mysql':return [
  2848. 'SET SESSION sql_warnings=1;',
  2849. 'SET NAMES utf8mb4;',
  2850. 'SET SESSION sql_mode = "ANSI,TRADITIONAL";',
  2851. ];
  2852. case 'pgsql':return [
  2853. "SET NAMES 'UTF8';",
  2854. ];
  2855. case 'sqlsrv':return [
  2856. ];
  2857. }
  2858. }
  2859. private function getOptions(): array
  2860. {
  2861. $options = array(
  2862. \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
  2863. \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
  2864. );
  2865. switch ($this->driver) {
  2866. case 'mysql':return $options + [
  2867. \PDO::ATTR_EMULATE_PREPARES => false,
  2868. \PDO::MYSQL_ATTR_FOUND_ROWS => true,
  2869. \PDO::ATTR_PERSISTENT => true,
  2870. ];
  2871. case 'pgsql':return $options + [
  2872. \PDO::ATTR_EMULATE_PREPARES => false,
  2873. \PDO::ATTR_PERSISTENT => true,
  2874. ];
  2875. case 'sqlsrv':return $options + [
  2876. \PDO::SQLSRV_ATTR_DIRECT_QUERY => false,
  2877. \PDO::SQLSRV_ATTR_FETCHES_NUMERIC_TYPE => true,
  2878. ];
  2879. }
  2880. }
  2881. public function __construct(string $driver, string $address, int $port, string $database, string $username, string $password)
  2882. {
  2883. $this->driver = $driver;
  2884. $this->database = $database;
  2885. $dsn = $this->getDsn($address, $port, $database);
  2886. $options = $this->getOptions();
  2887. $this->pdo = new \PDO($dsn, $username, $password, $options);
  2888. $commands = $this->getCommands();
  2889. foreach ($commands as $command) {
  2890. $this->pdo->query($command);
  2891. }
  2892. $this->reflection = new GenericReflection($this->pdo, $driver, $database);
  2893. $this->definition = new GenericDefinition($this->pdo, $driver, $database);
  2894. $this->conditions = new ConditionsBuilder($driver);
  2895. $this->columns = new ColumnsBuilder($driver);
  2896. $this->converter = new DataConverter($driver);
  2897. }
  2898. public function pdo(): \PDO
  2899. {
  2900. return $this->pdo;
  2901. }
  2902. public function reflection(): GenericReflection
  2903. {
  2904. return $this->reflection;
  2905. }
  2906. public function definition(): GenericDefinition
  2907. {
  2908. return $this->definition;
  2909. }
  2910. private function addMiddlewareConditions(string $tableName, Condition $condition): Condition
  2911. {
  2912. $condition1 = VariableStore::get("authorization.conditions.$tableName");
  2913. if ($condition1) {
  2914. $condition = $condition->_and($condition1);
  2915. }
  2916. $condition2 = VariableStore::get("multiTenancy.conditions.$tableName");
  2917. if ($condition2) {
  2918. $condition = $condition->_and($condition2);
  2919. }
  2920. return $condition;
  2921. }
  2922. public function createSingle(ReflectedTable $table, array $columnValues) /*: ?String*/
  2923. {
  2924. $this->converter->convertColumnValues($table, $columnValues);
  2925. $insertColumns = $this->columns->getInsert($table, $columnValues);
  2926. $tableName = $table->getName();
  2927. $pkName = $table->getPk()->getName();
  2928. $parameters = array_values($columnValues);
  2929. $sql = 'INSERT INTO "' . $tableName . '" ' . $insertColumns;
  2930. $stmt = $this->query($sql, $parameters);
  2931. if (isset($columnValues[$pkName])) {
  2932. return $columnValues[$pkName];
  2933. }
  2934. switch ($this->driver) {
  2935. case 'mysql':
  2936. $stmt = $this->query('SELECT LAST_INSERT_ID()', []);
  2937. break;
  2938. }
  2939. $pkValue = $stmt->fetchColumn(0);
  2940. if ($this->driver == 'sqlsrv' && $table->getPk()->getType() == 'bigint') {
  2941. return (int) $pkValue;
  2942. }
  2943. return $pkValue;
  2944. }
  2945. public function selectSingle(ReflectedTable $table, array $columnNames, string $id) /*: ?array*/
  2946. {
  2947. $selectColumns = $this->columns->getSelect($table, $columnNames);
  2948. $tableName = $table->getName();
  2949. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  2950. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2951. $parameters = array();
  2952. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  2953. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
  2954. $stmt = $this->query($sql, $parameters);
  2955. $record = $stmt->fetch() ?: null;
  2956. if ($record === null) {
  2957. return null;
  2958. }
  2959. $records = array($record);
  2960. $this->converter->convertRecords($table, $columnNames, $records);
  2961. return $records[0];
  2962. }
  2963. public function selectMultiple(ReflectedTable $table, array $columnNames, array $ids): array
  2964. {
  2965. if (count($ids) == 0) {
  2966. return [];
  2967. }
  2968. $selectColumns = $this->columns->getSelect($table, $columnNames);
  2969. $tableName = $table->getName();
  2970. $condition = new ColumnCondition($table->getPk(), 'in', implode(',', $ids));
  2971. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2972. $parameters = array();
  2973. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  2974. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '" ' . $whereClause;
  2975. $stmt = $this->query($sql, $parameters);
  2976. $records = $stmt->fetchAll();
  2977. $this->converter->convertRecords($table, $columnNames, $records);
  2978. return $records;
  2979. }
  2980. public function selectCount(ReflectedTable $table, Condition $condition): int
  2981. {
  2982. $tableName = $table->getName();
  2983. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2984. $parameters = array();
  2985. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  2986. $sql = 'SELECT COUNT(*) FROM "' . $tableName . '"' . $whereClause;
  2987. $stmt = $this->query($sql, $parameters);
  2988. return $stmt->fetchColumn(0);
  2989. }
  2990. public function selectAll(ReflectedTable $table, array $columnNames, Condition $condition, array $columnOrdering, int $offset, int $limit): array
  2991. {
  2992. if ($limit == 0) {
  2993. return array();
  2994. }
  2995. $selectColumns = $this->columns->getSelect($table, $columnNames);
  2996. $tableName = $table->getName();
  2997. $condition = $this->addMiddlewareConditions($tableName, $condition);
  2998. $parameters = array();
  2999. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3000. $orderBy = $this->columns->getOrderBy($table, $columnOrdering);
  3001. $offsetLimit = $this->columns->getOffsetLimit($offset, $limit);
  3002. $sql = 'SELECT ' . $selectColumns . ' FROM "' . $tableName . '"' . $whereClause . $orderBy . $offsetLimit;
  3003. $stmt = $this->query($sql, $parameters);
  3004. $records = $stmt->fetchAll();
  3005. $this->converter->convertRecords($table, $columnNames, $records);
  3006. return $records;
  3007. }
  3008. public function updateSingle(ReflectedTable $table, array $columnValues, string $id)
  3009. {
  3010. if (count($columnValues) == 0) {
  3011. return 0;
  3012. }
  3013. $this->converter->convertColumnValues($table, $columnValues);
  3014. $updateColumns = $this->columns->getUpdate($table, $columnValues);
  3015. $tableName = $table->getName();
  3016. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  3017. $condition = $this->addMiddlewareConditions($tableName, $condition);
  3018. $parameters = array_values($columnValues);
  3019. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3020. $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
  3021. $stmt = $this->query($sql, $parameters);
  3022. return $stmt->rowCount();
  3023. }
  3024. public function deleteSingle(ReflectedTable $table, string $id)
  3025. {
  3026. $tableName = $table->getName();
  3027. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  3028. $condition = $this->addMiddlewareConditions($tableName, $condition);
  3029. $parameters = array();
  3030. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3031. $sql = 'DELETE FROM "' . $tableName . '" ' . $whereClause;
  3032. $stmt = $this->query($sql, $parameters);
  3033. return $stmt->rowCount();
  3034. }
  3035. public function incrementSingle(ReflectedTable $table, array $columnValues, string $id)
  3036. {
  3037. if (count($columnValues) == 0) {
  3038. return 0;
  3039. }
  3040. $this->converter->convertColumnValues($table, $columnValues);
  3041. $updateColumns = $this->columns->getIncrement($table, $columnValues);
  3042. $tableName = $table->getName();
  3043. $condition = new ColumnCondition($table->getPk(), 'eq', $id);
  3044. $condition = $this->addMiddlewareConditions($tableName, $condition);
  3045. $parameters = array_values($columnValues);
  3046. $whereClause = $this->conditions->getWhereClause($condition, $parameters);
  3047. $sql = 'UPDATE "' . $tableName . '" SET ' . $updateColumns . $whereClause;
  3048. $stmt = $this->query($sql, $parameters);
  3049. return $stmt->rowCount();
  3050. }
  3051. private function query(string $sql, array $parameters): \PDOStatement
  3052. {
  3053. $stmt = $this->pdo->prepare($sql);
  3054. $stmt->execute($parameters);
  3055. return $stmt;
  3056. }
  3057. }
  3058. // file: src/Tqdev/PhpCrudApi/Database/GenericDefinition.php
  3059. class GenericDefinition
  3060. {
  3061. private $pdo;
  3062. private $driver;
  3063. private $database;
  3064. private $typeConverter;
  3065. private $reflection;
  3066. public function __construct(\PDO $pdo, string $driver, string $database)
  3067. {
  3068. $this->pdo = $pdo;
  3069. $this->driver = $driver;
  3070. $this->database = $database;
  3071. $this->typeConverter = new TypeConverter($driver);
  3072. $this->reflection = new GenericReflection($pdo, $driver, $database);
  3073. }
  3074. private function quote(string $identifier): string
  3075. {
  3076. return '"' . str_replace('"', '', $identifier) . '"';
  3077. }
  3078. public function getColumnType(ReflectedColumn $column, bool $update): string
  3079. {
  3080. if ($this->driver == 'pgsql' && !$update && $column->getPk() && $this->canAutoIncrement($column)) {
  3081. return 'serial';
  3082. }
  3083. $type = $this->typeConverter->fromJdbc($column->getType());
  3084. if ($column->hasPrecision() && $column->hasScale()) {
  3085. $size = '(' . $column->getPrecision() . ',' . $column->getScale() . ')';
  3086. } else if ($column->hasPrecision()) {
  3087. $size = '(' . $column->getPrecision() . ')';
  3088. } else if ($column->hasLength()) {
  3089. $size = '(' . $column->getLength() . ')';
  3090. } else {
  3091. $size = '';
  3092. }
  3093. $null = $this->getColumnNullType($column, $update);
  3094. $auto = $this->getColumnAutoIncrement($column, $update);
  3095. return $type . $size . $null . $auto;
  3096. }
  3097. private function getPrimaryKey(string $tableName): string
  3098. {
  3099. $pks = $this->reflection->getTablePrimaryKeys($tableName);
  3100. if (count($pks) == 1) {
  3101. return $pks[0];
  3102. }
  3103. return "";
  3104. }
  3105. private function canAutoIncrement(ReflectedColumn $column): bool
  3106. {
  3107. return in_array($column->getType(), ['integer', 'bigint']);
  3108. }
  3109. private function getColumnAutoIncrement(ReflectedColumn $column, bool $update): string
  3110. {
  3111. if (!$this->canAutoIncrement($column)) {
  3112. return '';
  3113. }
  3114. switch ($this->driver) {
  3115. case 'mysql':
  3116. return $column->getPk() ? ' AUTO_INCREMENT' : '';
  3117. case 'pgsql':
  3118. case 'sqlsrv':
  3119. return '';
  3120. }
  3121. }
  3122. private function getColumnNullType(ReflectedColumn $column, bool $update): string
  3123. {
  3124. if ($this->driver == 'pgsql' && $update) {
  3125. return '';
  3126. }
  3127. return $column->getNullable() ? ' NULL' : ' NOT NULL';
  3128. }
  3129. private function getTableRenameSQL(string $tableName, string $newTableName): string
  3130. {
  3131. $p1 = $this->quote($tableName);
  3132. $p2 = $this->quote($newTableName);
  3133. switch ($this->driver) {
  3134. case 'mysql':
  3135. return "RENAME TABLE $p1 TO $p2";
  3136. case 'pgsql':
  3137. return "ALTER TABLE $p1 RENAME TO $p2";
  3138. case 'sqlsrv':
  3139. return "EXEC sp_rename $p1, $p2";
  3140. }
  3141. }
  3142. private function getColumnRenameSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3143. {
  3144. $p1 = $this->quote($tableName);
  3145. $p2 = $this->quote($columnName);
  3146. $p3 = $this->quote($newColumn->getName());
  3147. switch ($this->driver) {
  3148. case 'mysql':
  3149. $p4 = $this->getColumnType($newColumn, true);
  3150. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3151. case 'pgsql':
  3152. return "ALTER TABLE $p1 RENAME COLUMN $p2 TO $p3";
  3153. case 'sqlsrv':
  3154. $p4 = $this->quote($tableName . '.' . $columnName);
  3155. return "EXEC sp_rename $p4, $p3, 'COLUMN'";
  3156. }
  3157. }
  3158. private function getColumnRetypeSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3159. {
  3160. $p1 = $this->quote($tableName);
  3161. $p2 = $this->quote($columnName);
  3162. $p3 = $this->quote($newColumn->getName());
  3163. $p4 = $this->getColumnType($newColumn, true);
  3164. switch ($this->driver) {
  3165. case 'mysql':
  3166. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3167. case 'pgsql':
  3168. return "ALTER TABLE $p1 ALTER COLUMN $p3 TYPE $p4";
  3169. case 'sqlsrv':
  3170. return "ALTER TABLE $p1 ALTER COLUMN $p3 $p4";
  3171. }
  3172. }
  3173. private function getSetColumnNullableSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3174. {
  3175. $p1 = $this->quote($tableName);
  3176. $p2 = $this->quote($columnName);
  3177. $p3 = $this->quote($newColumn->getName());
  3178. $p4 = $this->getColumnType($newColumn, true);
  3179. switch ($this->driver) {
  3180. case 'mysql':
  3181. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3182. case 'pgsql':
  3183. $p5 = $newColumn->getNullable() ? 'DROP NOT NULL' : 'SET NOT NULL';
  3184. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p5";
  3185. case 'sqlsrv':
  3186. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
  3187. }
  3188. }
  3189. private function getSetColumnPkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3190. {
  3191. $p1 = $this->quote($tableName);
  3192. $p2 = $this->quote($columnName);
  3193. $p3 = $this->quote($tableName . '_pkey');
  3194. switch ($this->driver) {
  3195. case 'mysql':
  3196. $p4 = $newColumn->getPk() ? "ADD PRIMARY KEY ($p2)" : 'DROP PRIMARY KEY';
  3197. return "ALTER TABLE $p1 $p4";
  3198. case 'pgsql':
  3199. case 'sqlsrv':
  3200. $p4 = $newColumn->getPk() ? "ADD CONSTRAINT $p3 PRIMARY KEY ($p2)" : "DROP CONSTRAINT $p3";
  3201. return "ALTER TABLE $p1 $p4";
  3202. }
  3203. }
  3204. private function getSetColumnPkSequenceSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3205. {
  3206. $p1 = $this->quote($tableName);
  3207. $p2 = $this->quote($columnName);
  3208. $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
  3209. switch ($this->driver) {
  3210. case 'mysql':
  3211. return "select 1";
  3212. case 'pgsql':
  3213. return $newColumn->getPk() ? "CREATE SEQUENCE $p3 OWNED BY $p1.$p2" : "DROP SEQUENCE $p3";
  3214. case 'sqlsrv':
  3215. return $newColumn->getPk() ? "CREATE SEQUENCE $p3" : "DROP SEQUENCE $p3";
  3216. }
  3217. }
  3218. private function getSetColumnPkSequenceStartSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3219. {
  3220. $p1 = $this->quote($tableName);
  3221. $p2 = $this->quote($columnName);
  3222. switch ($this->driver) {
  3223. case 'mysql':
  3224. return "select 1";
  3225. case 'pgsql':
  3226. $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
  3227. return "SELECT setval($p3, (SELECT max($p2)+1 FROM $p1));";
  3228. case 'sqlsrv':
  3229. $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
  3230. $p4 = $this->pdo->query("SELECT max($p2)+1 FROM $p1")->fetchColumn();
  3231. return "ALTER SEQUENCE $p3 RESTART WITH $p4";
  3232. }
  3233. }
  3234. private function getSetColumnPkDefaultSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3235. {
  3236. $p1 = $this->quote($tableName);
  3237. $p2 = $this->quote($columnName);
  3238. switch ($this->driver) {
  3239. case 'mysql':
  3240. $p3 = $this->quote($newColumn->getName());
  3241. $p4 = $this->getColumnType($newColumn, true);
  3242. return "ALTER TABLE $p1 CHANGE $p2 $p3 $p4";
  3243. case 'pgsql':
  3244. if ($newColumn->getPk()) {
  3245. $p3 = $this->pdo->quote($tableName . '_' . $columnName . '_seq');
  3246. $p4 = "SET DEFAULT nextval($p3)";
  3247. } else {
  3248. $p4 = 'DROP DEFAULT';
  3249. }
  3250. return "ALTER TABLE $p1 ALTER COLUMN $p2 $p4";
  3251. case 'sqlsrv':
  3252. $p3 = $this->quote($tableName . '_' . $columnName . '_seq');
  3253. $p4 = $this->quote($tableName . '_' . $columnName . '_def');
  3254. if ($newColumn->getPk()) {
  3255. return "ALTER TABLE $p1 ADD CONSTRAINT $p4 DEFAULT NEXT VALUE FOR $p3 FOR $p2";
  3256. } else {
  3257. return "ALTER TABLE $p1 DROP CONSTRAINT $p4";
  3258. }
  3259. }
  3260. }
  3261. private function getAddColumnFkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3262. {
  3263. $p1 = $this->quote($tableName);
  3264. $p2 = $this->quote($columnName);
  3265. $p3 = $this->quote($tableName . '_' . $columnName . '_fkey');
  3266. $p4 = $this->quote($newColumn->getFk());
  3267. $p5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
  3268. return "ALTER TABLE $p1 ADD CONSTRAINT $p3 FOREIGN KEY ($p2) REFERENCES $p4 ($p5)";
  3269. }
  3270. private function getRemoveColumnFkConstraintSQL(string $tableName, string $columnName, ReflectedColumn $newColumn): string
  3271. {
  3272. $p1 = $this->quote($tableName);
  3273. $p2 = $this->quote($tableName . '_' . $columnName . '_fkey');
  3274. switch ($this->driver) {
  3275. case 'mysql':
  3276. return "ALTER TABLE $p1 DROP FOREIGN KEY $p2";
  3277. case 'pgsql':
  3278. case 'sqlsrv':
  3279. return "ALTER TABLE $p1 DROP CONSTRAINT $p2";
  3280. }
  3281. }
  3282. private function getAddTableSQL(ReflectedTable $newTable): string
  3283. {
  3284. $tableName = $newTable->getName();
  3285. $p1 = $this->quote($tableName);
  3286. $fields = [];
  3287. $constraints = [];
  3288. foreach ($newTable->getColumnNames() as $columnName) {
  3289. $pkColumn = $this->getPrimaryKey($tableName);
  3290. $newColumn = $newTable->getColumn($columnName);
  3291. $f1 = $this->quote($columnName);
  3292. $f2 = $this->getColumnType($newColumn, false);
  3293. $f3 = $this->quote($tableName . '_' . $columnName . '_fkey');
  3294. $f4 = $this->quote($newColumn->getFk());
  3295. $f5 = $this->quote($this->getPrimaryKey($newColumn->getFk()));
  3296. $f6 = $this->quote($tableName . '_' . $pkColumn . '_pkey');
  3297. $fields[] = "$f1 $f2";
  3298. if ($newColumn->getPk()) {
  3299. $constraints[] = "CONSTRAINT $f6 PRIMARY KEY ($f1)";
  3300. }
  3301. if ($newColumn->getFk()) {
  3302. $constraints[] = "CONSTRAINT $f3 FOREIGN KEY ($f1) REFERENCES $f4 ($f5)";
  3303. }
  3304. }
  3305. $p2 = implode(',', array_merge($fields, $constraints));
  3306. return "CREATE TABLE $p1 ($p2);";
  3307. }
  3308. private function getAddColumnSQL(string $tableName, ReflectedColumn $newColumn): string
  3309. {
  3310. $p1 = $this->quote($tableName);
  3311. $p2 = $this->quote($newColumn->getName());
  3312. $p3 = $this->getColumnType($newColumn, false);
  3313. switch ($this->driver) {
  3314. case 'mysql':
  3315. case 'pgsql':
  3316. return "ALTER TABLE $p1 ADD COLUMN $p2 $p3";
  3317. case 'sqlsrv':
  3318. return "ALTER TABLE $p1 ADD $p2 $p3";
  3319. }
  3320. }
  3321. private function getRemoveTableSQL(string $tableName): string
  3322. {
  3323. $p1 = $this->quote($tableName);
  3324. switch ($this->driver) {
  3325. case 'mysql':
  3326. case 'pgsql':
  3327. return "DROP TABLE $p1 CASCADE;";
  3328. case 'sqlsrv':
  3329. return "DROP TABLE $p1;";
  3330. }
  3331. }
  3332. private function getRemoveColumnSQL(string $tableName, string $columnName): string
  3333. {
  3334. $p1 = $this->quote($tableName);
  3335. $p2 = $this->quote($columnName);
  3336. switch ($this->driver) {
  3337. case 'mysql':
  3338. case 'pgsql':
  3339. return "ALTER TABLE $p1 DROP COLUMN $p2 CASCADE;";
  3340. case 'sqlsrv':
  3341. return "ALTER TABLE $p1 DROP COLUMN $p2;";
  3342. }
  3343. }
  3344. public function renameTable(string $tableName, string $newTableName)
  3345. {
  3346. $sql = $this->getTableRenameSQL($tableName, $newTableName);
  3347. return $this->query($sql);
  3348. }
  3349. public function renameColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3350. {
  3351. $sql = $this->getColumnRenameSQL($tableName, $columnName, $newColumn);
  3352. return $this->query($sql);
  3353. }
  3354. public function retypeColumn(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3355. {
  3356. $sql = $this->getColumnRetypeSQL($tableName, $columnName, $newColumn);
  3357. return $this->query($sql);
  3358. }
  3359. public function setColumnNullable(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3360. {
  3361. $sql = $this->getSetColumnNullableSQL($tableName, $columnName, $newColumn);
  3362. return $this->query($sql);
  3363. }
  3364. public function addColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3365. {
  3366. $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
  3367. $this->query($sql);
  3368. if ($this->canAutoIncrement($newColumn)) {
  3369. $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
  3370. $this->query($sql);
  3371. $sql = $this->getSetColumnPkSequenceStartSQL($tableName, $columnName, $newColumn);
  3372. $this->query($sql);
  3373. $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
  3374. $this->query($sql);
  3375. }
  3376. return true;
  3377. }
  3378. public function removeColumnPrimaryKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3379. {
  3380. if ($this->canAutoIncrement($newColumn)) {
  3381. $sql = $this->getSetColumnPkDefaultSQL($tableName, $columnName, $newColumn);
  3382. $this->query($sql);
  3383. $sql = $this->getSetColumnPkSequenceSQL($tableName, $columnName, $newColumn);
  3384. $this->query($sql);
  3385. }
  3386. $sql = $this->getSetColumnPkConstraintSQL($tableName, $columnName, $newColumn);
  3387. $this->query($sql);
  3388. return true;
  3389. }
  3390. public function addColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3391. {
  3392. $sql = $this->getAddColumnFkConstraintSQL($tableName, $columnName, $newColumn);
  3393. return $this->query($sql);
  3394. }
  3395. public function removeColumnForeignKey(string $tableName, string $columnName, ReflectedColumn $newColumn)
  3396. {
  3397. $sql = $this->getRemoveColumnFkConstraintSQL($tableName, $columnName, $newColumn);
  3398. return $this->query($sql);
  3399. }
  3400. public function addTable(ReflectedTable $newTable)
  3401. {
  3402. $sql = $this->getAddTableSQL($newTable);
  3403. return $this->query($sql);
  3404. }
  3405. public function addColumn(string $tableName, ReflectedColumn $newColumn)
  3406. {
  3407. $sql = $this->getAddColumnSQL($tableName, $newColumn);
  3408. return $this->query($sql);
  3409. }
  3410. public function removeTable(string $tableName)
  3411. {
  3412. $sql = $this->getRemoveTableSQL($tableName);
  3413. return $this->query($sql);
  3414. }
  3415. public function removeColumn(string $tableName, string $columnName)
  3416. {
  3417. $sql = $this->getRemoveColumnSQL($tableName, $columnName);
  3418. return $this->query($sql);
  3419. }
  3420. private function query(string $sql): bool
  3421. {
  3422. $stmt = $this->pdo->prepare($sql);
  3423. return $stmt->execute();
  3424. }
  3425. }
  3426. // file: src/Tqdev/PhpCrudApi/Database/GenericReflection.php
  3427. class GenericReflection
  3428. {
  3429. private $pdo;
  3430. private $driver;
  3431. private $database;
  3432. private $typeConverter;
  3433. public function __construct(\PDO $pdo, string $driver, string $database)
  3434. {
  3435. $this->pdo = $pdo;
  3436. $this->driver = $driver;
  3437. $this->database = $database;
  3438. $this->typeConverter = new TypeConverter($driver);
  3439. }
  3440. public function getIgnoredTables(): array
  3441. {
  3442. switch ($this->driver) {
  3443. case 'mysql':return [];
  3444. case 'pgsql':return ['spatial_ref_sys', 'raster_columns', 'raster_overviews', 'geography_columns', 'geometry_columns'];
  3445. case 'sqlsrv':return [];
  3446. }
  3447. }
  3448. private function getTablesSQL(): string
  3449. {
  3450. switch ($this->driver) {
  3451. case 'mysql':return 'SELECT "TABLE_NAME", "TABLE_TYPE" FROM "INFORMATION_SCHEMA"."TABLES" WHERE "TABLE_TYPE" IN (\'BASE TABLE\' , \'VIEW\') AND "TABLE_SCHEMA" = ? ORDER BY BINARY "TABLE_NAME"';
  3452. case 'pgsql':return 'SELECT c.relname as "TABLE_NAME", c.relkind as "TABLE_TYPE" FROM pg_catalog.pg_class c LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN (\'r\', \'v\') AND n.nspname <> \'pg_catalog\' AND n.nspname <> \'information_schema\' AND n.nspname !~ \'^pg_toast\' AND pg_catalog.pg_table_is_visible(c.oid) AND \'\' <> ? ORDER BY "TABLE_NAME";';
  3453. case 'sqlsrv':return 'SELECT o.name as "TABLE_NAME", o.xtype as "TABLE_TYPE" FROM sysobjects o WHERE o.xtype IN (\'U\', \'V\') ORDER BY "TABLE_NAME"';
  3454. }
  3455. }
  3456. private function getTableColumnsSQL(): string
  3457. {
  3458. switch ($this->driver) {
  3459. case 'mysql':return 'SELECT "COLUMN_NAME", "IS_NULLABLE", "DATA_TYPE", "CHARACTER_MAXIMUM_LENGTH", "NUMERIC_PRECISION", "NUMERIC_SCALE" FROM "INFORMATION_SCHEMA"."COLUMNS" WHERE "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
  3460. case 'pgsql':return 'SELECT a.attname AS "COLUMN_NAME", case when a.attnotnull then \'NO\' else \'YES\' end as "IS_NULLABLE", pg_catalog.format_type(a.atttypid, -1) as "DATA_TYPE", case when a.atttypmod < 0 then NULL else a.atttypmod-4 end as "CHARACTER_MAXIMUM_LENGTH", case when a.atttypid != 1700 then NULL else ((a.atttypmod - 4) >> 16) & 65535 end as "NUMERIC_PRECISION", case when a.atttypid != 1700 then NULL else (a.atttypmod - 4) & 65535 end as "NUMERIC_SCALE" FROM pg_attribute a JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND a.attnum > 0 AND NOT a.attisdropped;';
  3461. case 'sqlsrv':return 'SELECT c.name AS "COLUMN_NAME", c.is_nullable AS "IS_NULLABLE", t.Name AS "DATA_TYPE", (c.max_length/2) AS "CHARACTER_MAXIMUM_LENGTH", c.precision AS "NUMERIC_PRECISION", c.scale AS "NUMERIC_SCALE" FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id WHERE c.object_id = OBJECT_ID(?) AND \'\' <> ?';
  3462. }
  3463. }
  3464. private function getTablePrimaryKeysSQL(): string
  3465. {
  3466. switch ($this->driver) {
  3467. case 'mysql':return 'SELECT "COLUMN_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "CONSTRAINT_NAME" = \'PRIMARY\' AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
  3468. case 'pgsql':return 'SELECT a.attname AS "COLUMN_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'p\'';
  3469. case 'sqlsrv':return 'SELECT c.NAME as "COLUMN_NAME" FROM sys.key_constraints kc inner join sys.objects t on t.object_id = kc.parent_object_id INNER JOIN sys.index_columns ic ON kc.parent_object_id = ic.object_id and kc.unique_index_id = ic.index_id INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE kc.type = \'PK\' and t.object_id = OBJECT_ID(?) and \'\' <> ?';
  3470. }
  3471. }
  3472. private function getTableForeignKeysSQL(): string
  3473. {
  3474. switch ($this->driver) {
  3475. case 'mysql':return 'SELECT "COLUMN_NAME", "REFERENCED_TABLE_NAME" FROM "INFORMATION_SCHEMA"."KEY_COLUMN_USAGE" WHERE "REFERENCED_TABLE_NAME" IS NOT NULL AND "TABLE_NAME" = ? AND "TABLE_SCHEMA" = ?';
  3476. case 'pgsql':return 'SELECT a.attname AS "COLUMN_NAME", c.confrelid::regclass::text AS "REFERENCED_TABLE_NAME" FROM pg_attribute a JOIN pg_constraint c ON (c.conrelid, c.conkey[1]) = (a.attrelid, a.attnum) JOIN pg_class pgc ON pgc.oid = a.attrelid WHERE pgc.relname = ? AND \'\' <> ? AND c.contype = \'f\'';
  3477. case 'sqlsrv':return 'SELECT COL_NAME(fc.parent_object_id, fc.parent_column_id) AS "COLUMN_NAME", OBJECT_NAME (f.referenced_object_id) AS "REFERENCED_TABLE_NAME" FROM sys.foreign_keys AS f INNER JOIN sys.foreign_key_columns AS fc ON f.OBJECT_ID = fc.constraint_object_id WHERE f.parent_object_id = OBJECT_ID(?) and \'\' <> ?';
  3478. }
  3479. }
  3480. public function getDatabaseName(): string
  3481. {
  3482. return $this->database;
  3483. }
  3484. public function getTables(): array
  3485. {
  3486. $sql = $this->getTablesSQL();
  3487. $results = $this->query($sql, [$this->database]);
  3488. foreach ($results as &$result) {
  3489. switch ($this->driver) {
  3490. case 'mysql':
  3491. $map = ['BASE TABLE' => 'table', 'VIEW' => 'view'];
  3492. $result['TABLE_TYPE'] = $map[$result['TABLE_TYPE']];
  3493. break;
  3494. case 'pgsql':
  3495. $map = ['r' => 'table', 'v' => 'view'];
  3496. $result['TABLE_TYPE'] = $map[$result['TABLE_TYPE']];
  3497. break;
  3498. case 'sqlsrv':
  3499. $map = ['U' => 'table', 'V' => 'view'];
  3500. $result['TABLE_TYPE'] = $map[trim($result['TABLE_TYPE'])];
  3501. break;
  3502. }
  3503. }
  3504. return $results;
  3505. }
  3506. public function getTableColumns(string $tableName, string $type): array
  3507. {
  3508. $sql = $this->getTableColumnsSQL();
  3509. $results = $this->query($sql, [$tableName, $this->database]);
  3510. if ($type == 'view') {
  3511. foreach ($results as &$result) {
  3512. $result['IS_NULLABLE'] = false;
  3513. }
  3514. }
  3515. return $results;
  3516. }
  3517. public function getTablePrimaryKeys(string $tableName): array
  3518. {
  3519. $sql = $this->getTablePrimaryKeysSQL();
  3520. $results = $this->query($sql, [$tableName, $this->database]);
  3521. $primaryKeys = [];
  3522. foreach ($results as $result) {
  3523. $primaryKeys[] = $result['COLUMN_NAME'];
  3524. }
  3525. return $primaryKeys;
  3526. }
  3527. public function getTableForeignKeys(string $tableName): array
  3528. {
  3529. $sql = $this->getTableForeignKeysSQL();
  3530. $results = $this->query($sql, [$tableName, $this->database]);
  3531. $foreignKeys = [];
  3532. foreach ($results as $result) {
  3533. $foreignKeys[$result['COLUMN_NAME']] = $result['REFERENCED_TABLE_NAME'];
  3534. }
  3535. return $foreignKeys;
  3536. }
  3537. public function toJdbcType(string $type, int $size): string
  3538. {
  3539. return $this->typeConverter->toJdbc($type, $size);
  3540. }
  3541. private function query(string $sql, array $parameters): array
  3542. {
  3543. $stmt = $this->pdo->prepare($sql);
  3544. $stmt->execute($parameters);
  3545. return $stmt->fetchAll();
  3546. }
  3547. }
  3548. // file: src/Tqdev/PhpCrudApi/Database/TypeConverter.php
  3549. class TypeConverter
  3550. {
  3551. private $driver;
  3552. public function __construct(string $driver)
  3553. {
  3554. $this->driver = $driver;
  3555. }
  3556. private $fromJdbc = [
  3557. 'mysql' => [
  3558. 'clob' => 'longtext',
  3559. 'boolean' => 'bit',
  3560. 'blob' => 'longblob',
  3561. 'timestamp' => 'datetime',
  3562. ],
  3563. 'pgsql' => [
  3564. 'clob' => 'text',
  3565. 'blob' => 'bytea',
  3566. ],
  3567. 'sqlsrv' => [
  3568. 'boolean' => 'bit',
  3569. 'varchar' => 'nvarchar',
  3570. 'clob' => 'ntext',
  3571. 'blob' => 'image',
  3572. ],
  3573. ];
  3574. private $toJdbc = [
  3575. 'simplified' => [
  3576. 'char' => 'varchar',
  3577. 'longvarchar' => 'clob',
  3578. 'nchar' => 'varchar',
  3579. 'nvarchar' => 'varchar',
  3580. 'longnvarchar' => 'clob',
  3581. 'binary' => 'varbinary',
  3582. 'longvarbinary' => 'blob',
  3583. 'tinyint' => 'integer',
  3584. 'smallint' => 'integer',
  3585. 'real' => 'float',
  3586. 'numeric' => 'decimal',
  3587. 'time_with_timezone' => 'time',
  3588. 'timestamp_with_timezone' => 'timestamp',
  3589. ],
  3590. 'mysql' => [
  3591. 'bit' => 'boolean',
  3592. 'tinyblob' => 'blob',
  3593. 'mediumblob' => 'blob',
  3594. 'longblob' => 'blob',
  3595. 'tinytext' => 'clob',
  3596. 'mediumtext' => 'clob',
  3597. 'longtext' => 'clob',
  3598. 'text' => 'clob',
  3599. 'mediumint' => 'integer',
  3600. 'int' => 'integer',
  3601. 'polygon' => 'geometry',
  3602. 'point' => 'geometry',
  3603. 'datetime' => 'timestamp',
  3604. 'year' => 'integer',
  3605. 'enum' => 'varchar',
  3606. 'json' => 'clob',
  3607. ],
  3608. 'pgsql' => [
  3609. 'bigserial' => 'bigint',
  3610. 'bit varying' => 'bit',
  3611. 'box' => 'geometry',
  3612. 'bytea' => 'blob',
  3613. 'character varying' => 'varchar',
  3614. 'character' => 'char',
  3615. 'cidr' => 'varchar',
  3616. 'circle' => 'geometry',
  3617. 'double precision' => 'double',
  3618. 'inet' => 'integer',
  3619. 'json' => 'clob',
  3620. 'jsonb' => 'clob',
  3621. 'line' => 'geometry',
  3622. 'lseg' => 'geometry',
  3623. 'macaddr' => 'varchar',
  3624. 'money' => 'decimal',
  3625. 'path' => 'geometry',
  3626. 'point' => 'geometry',
  3627. 'polygon' => 'geometry',
  3628. 'real' => 'float',
  3629. 'serial' => 'integer',
  3630. 'text' => 'clob',
  3631. 'time without time zone' => 'time',
  3632. 'time with time zone' => 'time_with_timezone',
  3633. 'timestamp without time zone' => 'timestamp',
  3634. 'timestamp with time zone' => 'timestamp_with_timezone',
  3635. 'uuid' => 'char',
  3636. 'xml' => 'clob',
  3637. ],
  3638. 'sqlsrv' => [
  3639. 'varbinary(0)' => 'blob',
  3640. 'bit' => 'boolean',
  3641. 'datetime' => 'timestamp',
  3642. 'datetime2' => 'timestamp',
  3643. 'float' => 'double',
  3644. 'image' => 'blob',
  3645. 'int' => 'integer',
  3646. 'money' => 'decimal',
  3647. 'ntext' => 'clob',
  3648. 'smalldatetime' => 'timestamp',
  3649. 'smallmoney' => 'decimal',
  3650. 'text' => 'clob',
  3651. 'timestamp' => 'binary',
  3652. 'udt' => 'varbinary',
  3653. 'uniqueidentifier' => 'char',
  3654. 'xml' => 'clob',
  3655. ],
  3656. ];
  3657. private $valid = [
  3658. 'bigint' => true,
  3659. 'binary' => true,
  3660. 'bit' => true,
  3661. 'blob' => true,
  3662. 'boolean' => true,
  3663. 'char' => true,
  3664. 'clob' => true,
  3665. 'date' => true,
  3666. 'decimal' => true,
  3667. 'distinct' => true,
  3668. 'double' => true,
  3669. 'float' => true,
  3670. 'integer' => true,
  3671. 'longnvarchar' => true,
  3672. 'longvarbinary' => true,
  3673. 'longvarchar' => true,
  3674. 'nchar' => true,
  3675. 'nclob' => true,
  3676. 'numeric' => true,
  3677. 'nvarchar' => true,
  3678. 'real' => true,
  3679. 'smallint' => true,
  3680. 'time' => true,
  3681. 'time_with_timezone' => true,
  3682. 'timestamp' => true,
  3683. 'timestamp_with_timezone' => true,
  3684. 'tinyint' => true,
  3685. 'varbinary' => true,
  3686. 'varchar' => true,
  3687. 'geometry' => true,
  3688. ];
  3689. public function toJdbc(string $type, int $size): string
  3690. {
  3691. $jdbcType = strtolower($type);
  3692. if (isset($this->toJdbc[$this->driver]["$jdbcType($size)"])) {
  3693. $jdbcType = $this->toJdbc[$this->driver]["$jdbcType($size)"];
  3694. }
  3695. if (isset($this->toJdbc[$this->driver][$jdbcType])) {
  3696. $jdbcType = $this->toJdbc[$this->driver][$jdbcType];
  3697. }
  3698. if (isset($this->toJdbc['simplified'][$jdbcType])) {
  3699. $jdbcType = $this->toJdbc['simplified'][$jdbcType];
  3700. }
  3701. if (!isset($this->valid[$jdbcType])) {
  3702. throw new \Exception("Unsupported type '$jdbcType' for driver '$this->driver'");
  3703. }
  3704. return $jdbcType;
  3705. }
  3706. public function fromJdbc(string $type): string
  3707. {
  3708. $jdbcType = strtolower($type);
  3709. if (isset($this->fromJdbc[$this->driver][$jdbcType])) {
  3710. $jdbcType = $this->fromJdbc[$this->driver][$jdbcType];
  3711. }
  3712. return $jdbcType;
  3713. }
  3714. }
  3715. // file: src/Tqdev/PhpCrudApi/Middleware/Base/Middleware.php
  3716. abstract class Middleware implements MiddlewareInterface
  3717. {
  3718. protected $next;
  3719. protected $responder;
  3720. private $properties;
  3721. public function __construct(Router $router, Responder $responder, array $properties)
  3722. {
  3723. $router->load($this);
  3724. $this->responder = $responder;
  3725. $this->properties = $properties;
  3726. }
  3727. protected function getArrayProperty(string $key, string $default): array
  3728. {
  3729. return array_filter(array_map('trim', explode(',', $this->getProperty($key, $default))));
  3730. }
  3731. protected function getProperty(string $key, $default)
  3732. {
  3733. return isset($this->properties[$key]) ? $this->properties[$key] : $default;
  3734. }
  3735. }
  3736. // file: src/Tqdev/PhpCrudApi/Middleware/Communication/VariableStore.php
  3737. class VariableStore
  3738. {
  3739. static $values = array();
  3740. public static function get(string $key)
  3741. {
  3742. if (isset(self::$values[$key])) {
  3743. return self::$values[$key];
  3744. }
  3745. return null;
  3746. }
  3747. public static function set(string $key, /* object */ $value)
  3748. {
  3749. self::$values[$key] = $value;
  3750. }
  3751. }
  3752. // file: src/Tqdev/PhpCrudApi/Middleware/Router/Router.php
  3753. interface Router extends RequestHandlerInterface
  3754. {
  3755. public function register(string $method, string $path, array $handler);
  3756. public function load(Middleware $middleware);
  3757. public function route(ServerRequestInterface $request): ResponseInterface;
  3758. }
  3759. // file: src/Tqdev/PhpCrudApi/Middleware/Router/SimpleRouter.php
  3760. class SimpleRouter implements Router
  3761. {
  3762. private $basePath;
  3763. private $responder;
  3764. private $cache;
  3765. private $ttl;
  3766. private $debug;
  3767. private $registration;
  3768. private $routes;
  3769. private $routeHandlers;
  3770. private $middlewares;
  3771. public function __construct(string $basePath, Responder $responder, Cache $cache, int $ttl, bool $debug)
  3772. {
  3773. $this->basePath = $basePath;
  3774. $this->responder = $responder;
  3775. $this->cache = $cache;
  3776. $this->ttl = $ttl;
  3777. $this->debug = $debug;
  3778. $this->registration = true;
  3779. $this->routes = $this->loadPathTree();
  3780. $this->routeHandlers = [];
  3781. $this->middlewares = array();
  3782. }
  3783. private function loadPathTree(): PathTree
  3784. {
  3785. $data = $this->cache->get('PathTree');
  3786. if ($data != '') {
  3787. $tree = PathTree::fromJson(json_decode(gzuncompress($data)));
  3788. $this->registration = false;
  3789. } else {
  3790. $tree = new PathTree();
  3791. }
  3792. return $tree;
  3793. }
  3794. public function register(string $method, string $path, array $handler)
  3795. {
  3796. $routeNumber = count($this->routeHandlers);
  3797. $this->routeHandlers[$routeNumber] = $handler;
  3798. if ($this->registration) {
  3799. $parts = explode('/', trim($path, '/'));
  3800. array_unshift($parts, $method);
  3801. $this->routes->put($parts, $routeNumber);
  3802. }
  3803. }
  3804. public function load(Middleware $middleware) /*: void*/
  3805. {
  3806. array_push($this->middlewares, $middleware);
  3807. }
  3808. public function route(ServerRequestInterface $request): ResponseInterface
  3809. {
  3810. if ($this->registration) {
  3811. $data = gzcompress(json_encode($this->routes, JSON_UNESCAPED_UNICODE));
  3812. $this->cache->set('PathTree', $data, $this->ttl);
  3813. }
  3814. return $this->handle($request);
  3815. }
  3816. private function getRouteNumbers(ServerRequestInterface $request): array
  3817. {
  3818. $method = strtoupper($request->getMethod());
  3819. $path = array();
  3820. $segment = $method;
  3821. for ($i = 1; $segment; $i++) {
  3822. array_push($path, $segment);
  3823. $segment = RequestUtils::getPathSegment($request, $i);
  3824. }
  3825. return $this->routes->match($path);
  3826. }
  3827. private function removeBasePath(ServerRequestInterface $request): ServerRequestInterface
  3828. {
  3829. if ($this->basePath) {
  3830. $path = $request->getUri()->getPath();
  3831. $basePath = rtrim($this->basePath, '/');
  3832. if (substr($path, 0, strlen($basePath)) == $basePath) {
  3833. $path = substr($path, strlen($basePath));
  3834. $request = $request->withUri($request->getUri()->withPath($path));
  3835. }
  3836. } elseif (isset($_SERVER['PATH_INFO'])) {
  3837. $path = $_SERVER['PATH_INFO'];
  3838. $request = $request->withUri($request->getUri()->withPath($path));
  3839. }
  3840. return $request;
  3841. }
  3842. public function handle(ServerRequestInterface $request): ResponseInterface
  3843. {
  3844. $request = $this->removeBasePath($request);
  3845. if (count($this->middlewares)) {
  3846. $handler = array_pop($this->middlewares);
  3847. return $handler->process($request, $this);
  3848. }
  3849. $routeNumbers = $this->getRouteNumbers($request);
  3850. if (count($routeNumbers) == 0) {
  3851. return $this->responder->error(ErrorCode::ROUTE_NOT_FOUND, $request->getUri()->getPath());
  3852. }
  3853. try {
  3854. $response = call_user_func($this->routeHandlers[$routeNumbers[0]], $request);
  3855. } catch (\PDOException $e) {
  3856. if (strpos(strtolower($e->getMessage()), 'duplicate') !== false) {
  3857. $response = $this->responder->error(ErrorCode::DUPLICATE_KEY_EXCEPTION, '');
  3858. } elseif (strpos(strtolower($e->getMessage()), 'default value') !== false) {
  3859. $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  3860. } elseif (strpos(strtolower($e->getMessage()), 'allow nulls') !== false) {
  3861. $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  3862. } elseif (strpos(strtolower($e->getMessage()), 'constraint') !== false) {
  3863. $response = $this->responder->error(ErrorCode::DATA_INTEGRITY_VIOLATION, '');
  3864. }
  3865. if ($this->debug) {
  3866. $response = ResponseUtils::addExceptionHeaders($response, $e);
  3867. }
  3868. }
  3869. return $response;
  3870. }
  3871. }
  3872. // file: src/Tqdev/PhpCrudApi/Middleware/AjaxOnlyMiddleware.php
  3873. class AjaxOnlyMiddleware extends Middleware
  3874. {
  3875. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  3876. {
  3877. $method = $request->getMethod();
  3878. $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
  3879. if (!in_array($method, $excludeMethods)) {
  3880. $headerName = $this->getProperty('headerName', 'X-Requested-With');
  3881. $headerValue = $this->getProperty('headerValue', 'XMLHttpRequest');
  3882. if ($headerValue != RequestUtils::getHeader($request, $headerName)) {
  3883. return $this->responder->error(ErrorCode::ONLY_AJAX_REQUESTS_ALLOWED, $method);
  3884. }
  3885. }
  3886. return $next->handle($request);
  3887. }
  3888. }
  3889. // file: src/Tqdev/PhpCrudApi/Middleware/AuthorizationMiddleware.php
  3890. class AuthorizationMiddleware extends Middleware
  3891. {
  3892. private $reflection;
  3893. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  3894. {
  3895. parent::__construct($router, $responder, $properties);
  3896. $this->reflection = $reflection;
  3897. }
  3898. private function handleColumns(string $operation, string $tableName) /*: void*/
  3899. {
  3900. $columnHandler = $this->getProperty('columnHandler', '');
  3901. if ($columnHandler) {
  3902. $table = $this->reflection->getTable($tableName);
  3903. foreach ($table->getColumnNames() as $columnName) {
  3904. $allowed = call_user_func($columnHandler, $operation, $tableName, $columnName);
  3905. if (!$allowed) {
  3906. $table->removeColumn($columnName);
  3907. }
  3908. }
  3909. }
  3910. }
  3911. private function handleTable(string $operation, string $tableName) /*: void*/
  3912. {
  3913. if (!$this->reflection->hasTable($tableName)) {
  3914. return;
  3915. }
  3916. $tableHandler = $this->getProperty('tableHandler', '');
  3917. if ($tableHandler) {
  3918. $allowed = call_user_func($tableHandler, $operation, $tableName);
  3919. if (!$allowed) {
  3920. $this->reflection->removeTable($tableName);
  3921. } else {
  3922. $this->handleColumns($operation, $tableName);
  3923. }
  3924. }
  3925. }
  3926. private function handleRecords(string $operation, string $tableName) /*: void*/
  3927. {
  3928. if (!$this->reflection->hasTable($tableName)) {
  3929. return;
  3930. }
  3931. $recordHandler = $this->getProperty('recordHandler', '');
  3932. if ($recordHandler) {
  3933. $query = call_user_func($recordHandler, $operation, $tableName);
  3934. $filters = new FilterInfo();
  3935. $table = $this->reflection->getTable($tableName);
  3936. $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
  3937. parse_str($query, $params);
  3938. $condition = $filters->getCombinedConditions($table, $params);
  3939. VariableStore::set("authorization.conditions.$tableName", $condition);
  3940. }
  3941. }
  3942. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  3943. {
  3944. $path = RequestUtils::getPathSegment($request, 1);
  3945. $operation = RequestUtils::getOperation($request);
  3946. $tableNames = RequestUtils::getTableNames($request, $this->reflection);
  3947. foreach ($tableNames as $tableName) {
  3948. $this->handleTable($operation, $tableName);
  3949. if ($path == 'records') {
  3950. $this->handleRecords($operation, $tableName);
  3951. }
  3952. }
  3953. if ($path == 'openapi') {
  3954. VariableStore::set('authorization.tableHandler', $this->getProperty('tableHandler', ''));
  3955. VariableStore::set('authorization.columnHandler', $this->getProperty('columnHandler', ''));
  3956. }
  3957. return $next->handle($request);
  3958. }
  3959. }
  3960. // file: src/Tqdev/PhpCrudApi/Middleware/BasicAuthMiddleware.php
  3961. class BasicAuthMiddleware extends Middleware
  3962. {
  3963. private function hasCorrectPassword(string $username, string $password, array &$passwords): bool
  3964. {
  3965. $hash = isset($passwords[$username]) ? $passwords[$username] : false;
  3966. if ($hash && password_verify($password, $hash)) {
  3967. if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
  3968. $passwords[$username] = password_hash($password, PASSWORD_DEFAULT);
  3969. }
  3970. return true;
  3971. }
  3972. return false;
  3973. }
  3974. private function getValidUsername(string $username, string $password, string $passwordFile): string
  3975. {
  3976. $passwords = $this->readPasswords($passwordFile);
  3977. $valid = $this->hasCorrectPassword($username, $password, $passwords);
  3978. $this->writePasswords($passwordFile, $passwords);
  3979. return $valid ? $username : '';
  3980. }
  3981. private function readPasswords(string $passwordFile): array
  3982. {
  3983. $passwords = [];
  3984. $passwordLines = file($passwordFile);
  3985. foreach ($passwordLines as $passwordLine) {
  3986. if (strpos($passwordLine, ':') !== false) {
  3987. list($username, $hash) = explode(':', trim($passwordLine), 2);
  3988. if (strlen($hash) > 0 && $hash[0] != '$') {
  3989. $hash = password_hash($hash, PASSWORD_DEFAULT);
  3990. }
  3991. $passwords[$username] = $hash;
  3992. }
  3993. }
  3994. return $passwords;
  3995. }
  3996. private function writePasswords(string $passwordFile, array $passwords): bool
  3997. {
  3998. $success = false;
  3999. $passwordFileContents = '';
  4000. foreach ($passwords as $username => $hash) {
  4001. $passwordFileContents .= "$username:$hash\n";
  4002. }
  4003. if (file_get_contents($passwordFile) != $passwordFileContents) {
  4004. $success = file_put_contents($passwordFile, $passwordFileContents) !== false;
  4005. }
  4006. return $success;
  4007. }
  4008. private function getAuthorizationCredentials(ServerRequestInterface $request): string
  4009. {
  4010. if (isset($_SERVER['PHP_AUTH_USER'])) {
  4011. return $_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW'];
  4012. }
  4013. $header = RequestUtils::getHeader($request, 'Authorization');
  4014. $parts = explode(' ', trim($header), 2);
  4015. if (count($parts) != 2) {
  4016. return '';
  4017. }
  4018. if ($parts[0] != 'Basic') {
  4019. return '';
  4020. }
  4021. return base64_decode(strtr($parts[1], '-_', '+/'));
  4022. }
  4023. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4024. {
  4025. if (session_status() == PHP_SESSION_NONE) {
  4026. session_start();
  4027. }
  4028. $credentials = $this->getAuthorizationCredentials($request);
  4029. if ($credentials) {
  4030. list($username, $password) = array('', '');
  4031. if (strpos($credentials, ':') !== false) {
  4032. list($username, $password) = explode(':', $credentials, 2);
  4033. }
  4034. $passwordFile = $this->getProperty('passwordFile', '.htpasswd');
  4035. $validUser = $this->getValidUsername($username, $password, $passwordFile);
  4036. $_SESSION['username'] = $validUser;
  4037. if (!$validUser) {
  4038. return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, $username);
  4039. }
  4040. if (!headers_sent()) {
  4041. session_regenerate_id();
  4042. }
  4043. }
  4044. if (!isset($_SESSION['username']) || !$_SESSION['username']) {
  4045. $authenticationMode = $this->getProperty('mode', 'required');
  4046. if ($authenticationMode == 'required') {
  4047. $response = $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
  4048. $realm = $this->getProperty('realm', 'Username and password required');
  4049. $response = $response->withHeader('WWW-Authenticate', "Basic realm=\"$realm\"");
  4050. return $response;
  4051. }
  4052. }
  4053. return $next->handle($request);
  4054. }
  4055. }
  4056. // file: src/Tqdev/PhpCrudApi/Middleware/CorsMiddleware.php
  4057. class CorsMiddleware extends Middleware
  4058. {
  4059. private function isOriginAllowed(string $origin, string $allowedOrigins): bool
  4060. {
  4061. $found = false;
  4062. foreach (explode(',', $allowedOrigins) as $allowedOrigin) {
  4063. $hostname = preg_quote(strtolower(trim($allowedOrigin)));
  4064. $regex = '/^' . str_replace('\*', '.*', $hostname) . '$/';
  4065. if (preg_match($regex, $origin)) {
  4066. $found = true;
  4067. break;
  4068. }
  4069. }
  4070. return $found;
  4071. }
  4072. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4073. {
  4074. $method = $request->getMethod();
  4075. $origin = count($request->getHeader('Origin')) ? $request->getHeader('Origin')[0] : '';
  4076. $allowedOrigins = $this->getProperty('allowedOrigins', '*');
  4077. if ($origin && !$this->isOriginAllowed($origin, $allowedOrigins)) {
  4078. $response = $this->responder->error(ErrorCode::ORIGIN_FORBIDDEN, $origin);
  4079. } elseif ($method == 'OPTIONS') {
  4080. $response = ResponseFactory::fromStatus(ResponseFactory::OK);
  4081. $allowHeaders = $this->getProperty('allowHeaders', 'Content-Type, X-XSRF-TOKEN, X-Authorization');
  4082. if ($allowHeaders) {
  4083. $response = $response->withHeader('Access-Control-Allow-Headers', $allowHeaders);
  4084. }
  4085. $allowMethods = $this->getProperty('allowMethods', 'OPTIONS, GET, PUT, POST, DELETE, PATCH');
  4086. if ($allowMethods) {
  4087. $response = $response->withHeader('Access-Control-Allow-Methods', $allowMethods);
  4088. }
  4089. $allowCredentials = $this->getProperty('allowCredentials', 'true');
  4090. if ($allowCredentials) {
  4091. $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials);
  4092. }
  4093. $maxAge = $this->getProperty('maxAge', '1728000');
  4094. if ($maxAge) {
  4095. $response = $response->withHeader('Access-Control-Max-Age', $maxAge);
  4096. }
  4097. $exposeHeaders = $this->getProperty('exposeHeaders', '');
  4098. if ($exposeHeaders) {
  4099. $response = $response->withHeader('Access-Control-Expose-Headers', $exposeHeaders);
  4100. }
  4101. } else {
  4102. $response = $next->handle($request);
  4103. }
  4104. if ($origin) {
  4105. $allowCredentials = $this->getProperty('allowCredentials', 'true');
  4106. if ($allowCredentials) {
  4107. $response = $response->withHeader('Access-Control-Allow-Credentials', $allowCredentials);
  4108. }
  4109. $response = $response->withHeader('Access-Control-Allow-Origin', $origin);
  4110. }
  4111. return $response;
  4112. }
  4113. }
  4114. // file: src/Tqdev/PhpCrudApi/Middleware/CustomizationMiddleware.php
  4115. class CustomizationMiddleware extends Middleware
  4116. {
  4117. private $reflection;
  4118. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4119. {
  4120. parent::__construct($router, $responder, $properties);
  4121. $this->reflection = $reflection;
  4122. }
  4123. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4124. {
  4125. $operation = RequestUtils::getOperation($request);
  4126. $tableName = RequestUtils::getPathSegment($request, 2);
  4127. $beforeHandler = $this->getProperty('beforeHandler', '');
  4128. $environment = (object) array();
  4129. if ($beforeHandler !== '') {
  4130. $result = call_user_func($beforeHandler, $operation, $tableName, $request, $environment);
  4131. $request = $result ?: $request;
  4132. }
  4133. $response = $next->handle($request);
  4134. $afterHandler = $this->getProperty('afterHandler', '');
  4135. if ($afterHandler !== '') {
  4136. $result = call_user_func($afterHandler, $operation, $tableName, $response, $environment);
  4137. $response = $result ?: $response;
  4138. }
  4139. return $response;
  4140. }
  4141. }
  4142. // file: src/Tqdev/PhpCrudApi/Middleware/FirewallMiddleware.php
  4143. class FirewallMiddleware extends Middleware
  4144. {
  4145. private function ipMatch(string $ip, string $cidr): bool
  4146. {
  4147. if (strpos($cidr, '/') !== false) {
  4148. list($subnet, $mask) = explode('/', trim($cidr));
  4149. if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1)) == ip2long($subnet)) {
  4150. return true;
  4151. }
  4152. } else {
  4153. if (ip2long($ip) == ip2long($cidr)) {
  4154. return true;
  4155. }
  4156. }
  4157. return false;
  4158. }
  4159. private function isIpAllowed(string $ipAddress, string $allowedIpAddresses): bool
  4160. {
  4161. foreach (explode(',', $allowedIpAddresses) as $allowedIp) {
  4162. if ($this->ipMatch($ipAddress, $allowedIp)) {
  4163. return true;
  4164. }
  4165. }
  4166. return false;
  4167. }
  4168. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4169. {
  4170. $reverseProxy = $this->getProperty('reverseProxy', '');
  4171. if ($reverseProxy) {
  4172. $ipAddress = array_pop(explode(',', $request->getHeader('X-Forwarded-For')));
  4173. } elseif (isset($_SERVER['REMOTE_ADDR'])) {
  4174. $ipAddress = $_SERVER['REMOTE_ADDR'];
  4175. } else {
  4176. $ipAddress = '127.0.0.1';
  4177. }
  4178. $allowedIpAddresses = $this->getProperty('allowedIpAddresses', '');
  4179. if (!$this->isIpAllowed($ipAddress, $allowedIpAddresses)) {
  4180. $response = $this->responder->error(ErrorCode::TEMPORARY_OR_PERMANENTLY_BLOCKED, '');
  4181. } else {
  4182. $response = $next->handle($request);
  4183. }
  4184. return $response;
  4185. }
  4186. }
  4187. // file: src/Tqdev/PhpCrudApi/Middleware/IpAddressMiddleware.php
  4188. class IpAddressMiddleware extends Middleware
  4189. {
  4190. private $reflection;
  4191. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4192. {
  4193. parent::__construct($router, $responder, $properties);
  4194. $this->reflection = $reflection;
  4195. }
  4196. private function callHandler($record, string $operation, ReflectedTable $table) /*: object */
  4197. {
  4198. $context = (array) $record;
  4199. $columnNames = $this->getProperty('columns', '');
  4200. if ($columnNames) {
  4201. foreach (explode(',', $columnNames) as $columnName) {
  4202. if ($table->hasColumn($columnName)) {
  4203. if ($operation == 'create') {
  4204. $context[$columnName] = $_SERVER['REMOTE_ADDR'];
  4205. } else {
  4206. unset($context[$columnName]);
  4207. }
  4208. }
  4209. }
  4210. }
  4211. return (object) $context;
  4212. }
  4213. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4214. {
  4215. $operation = RequestUtils::getOperation($request);
  4216. if (in_array($operation, ['create', 'update', 'increment'])) {
  4217. $tableNames = $this->getProperty('tables', '');
  4218. $tableName = RequestUtils::getPathSegment($request, 2);
  4219. if (!$tableNames || in_array($tableName, explode(',', $tableNames))) {
  4220. if ($this->reflection->hasTable($tableName)) {
  4221. $record = $request->getParsedBody();
  4222. if ($record !== null) {
  4223. $table = $this->reflection->getTable($tableName);
  4224. if (is_array($record)) {
  4225. foreach ($record as &$r) {
  4226. $r = $this->callHandler($r, $operation, $table);
  4227. }
  4228. } else {
  4229. $record = $this->callHandler($record, $operation, $table);
  4230. }
  4231. $request = $request->withParsedBody($record);
  4232. }
  4233. }
  4234. }
  4235. }
  4236. return $next->handle($request);
  4237. }
  4238. }
  4239. // file: src/Tqdev/PhpCrudApi/Middleware/JoinLimitsMiddleware.php
  4240. class JoinLimitsMiddleware extends Middleware
  4241. {
  4242. private $reflection;
  4243. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4244. {
  4245. parent::__construct($router, $responder, $properties);
  4246. $this->reflection = $reflection;
  4247. }
  4248. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4249. {
  4250. $operation = RequestUtils::getOperation($request);
  4251. $params = RequestUtils::getParams($request);
  4252. if (in_array($operation, ['read', 'list']) && isset($params['join'])) {
  4253. $maxDepth = (int) $this->getProperty('depth', '3');
  4254. $maxTables = (int) $this->getProperty('tables', '10');
  4255. $maxRecords = (int) $this->getProperty('records', '1000');
  4256. $tableCount = 0;
  4257. $joinPaths = array();
  4258. for ($i = 0; $i < count($params['join']); $i++) {
  4259. $joinPath = array();
  4260. $tables = explode(',', $params['join'][$i]);
  4261. for ($depth = 0; $depth < min($maxDepth, count($tables)); $depth++) {
  4262. array_push($joinPath, $tables[$depth]);
  4263. $tableCount += 1;
  4264. if ($tableCount == $maxTables) {
  4265. break;
  4266. }
  4267. }
  4268. array_push($joinPaths, implode(',', $joinPath));
  4269. if ($tableCount == $maxTables) {
  4270. break;
  4271. }
  4272. }
  4273. $params['join'] = $joinPaths;
  4274. $request = RequestUtils::setParams($request, $params);
  4275. VariableStore::set("joinLimits.maxRecords", $maxRecords);
  4276. }
  4277. return $next->handle($request);
  4278. }
  4279. }
  4280. // file: src/Tqdev/PhpCrudApi/Middleware/JwtAuthMiddleware.php
  4281. class JwtAuthMiddleware extends Middleware
  4282. {
  4283. private function getVerifiedClaims(string $token, int $time, int $leeway, int $ttl, string $secret, array $requirements): array
  4284. {
  4285. $algorithms = array(
  4286. 'HS256' => 'sha256',
  4287. 'HS384' => 'sha384',
  4288. 'HS512' => 'sha512',
  4289. 'RS256' => 'sha256',
  4290. 'RS384' => 'sha384',
  4291. 'RS512' => 'sha512',
  4292. );
  4293. $token = explode('.', $token);
  4294. if (count($token) < 3) {
  4295. return array();
  4296. }
  4297. $header = json_decode(base64_decode(strtr($token[0], '-_', '+/')), true);
  4298. if (!$secret) {
  4299. return array();
  4300. }
  4301. if ($header['typ'] != 'JWT') {
  4302. return array();
  4303. }
  4304. $algorithm = $header['alg'];
  4305. if (!isset($algorithms[$algorithm])) {
  4306. return array();
  4307. }
  4308. if (!empty($requirements['alg']) && !in_array($algorithm, $requirements['alg'])) {
  4309. return array();
  4310. }
  4311. $hmac = $algorithms[$algorithm];
  4312. $signature = base64_decode(strtr($token[2], '-_', '+/'));
  4313. $data = "$token[0].$token[1]";
  4314. switch ($algorithm[0]) {
  4315. case 'H':
  4316. $hash = hash_hmac($hmac, $data, $secret, true);
  4317. if (function_exists('hash_equals')) {
  4318. $equals = hash_equals($signature, $hash);
  4319. } else {
  4320. $equals = $signature == $hash;
  4321. }
  4322. if (!$equals) {
  4323. return array();
  4324. }
  4325. break;
  4326. case 'R':
  4327. $equals = openssl_verify($data, $signature, $secret, $hmac) == 1;
  4328. if (!$equals) {
  4329. return array();
  4330. }
  4331. break;
  4332. }
  4333. $claims = json_decode(base64_decode(strtr($token[1], '-_', '+/')), true);
  4334. if (!$claims) {
  4335. return array();
  4336. }
  4337. foreach ($requirements as $field => $values) {
  4338. if (!empty($values)) {
  4339. if ($field != 'alg') {
  4340. if (!isset($claims[$field]) || !in_array($claims[$field], $values)) {
  4341. return array();
  4342. }
  4343. }
  4344. }
  4345. }
  4346. if (isset($claims['nbf']) && $time + $leeway < $claims['nbf']) {
  4347. return array();
  4348. }
  4349. if (isset($claims['iat']) && $time + $leeway < $claims['iat']) {
  4350. return array();
  4351. }
  4352. if (isset($claims['exp']) && $time - $leeway > $claims['exp']) {
  4353. return array();
  4354. }
  4355. if (isset($claims['iat']) && !isset($claims['exp'])) {
  4356. if ($time - $leeway > $claims['iat'] + $ttl) {
  4357. return array();
  4358. }
  4359. }
  4360. return $claims;
  4361. }
  4362. private function getClaims(string $token): array
  4363. {
  4364. $time = (int) $this->getProperty('time', time());
  4365. $leeway = (int) $this->getProperty('leeway', '5');
  4366. $ttl = (int) $this->getProperty('ttl', '30');
  4367. $secret = $this->getProperty('secret', '');
  4368. $requirements = array(
  4369. 'alg' => $this->getArrayProperty('algorithms', ''),
  4370. 'aud' => $this->getArrayProperty('audiences', ''),
  4371. 'iss' => $this->getArrayProperty('issuers', ''),
  4372. );
  4373. if (!$secret) {
  4374. return array();
  4375. }
  4376. return $this->getVerifiedClaims($token, $time, $leeway, $ttl, $secret, $requirements);
  4377. }
  4378. private function getAuthorizationToken(ServerRequestInterface $request): string
  4379. {
  4380. $headerName = $this->getProperty('header', 'X-Authorization');
  4381. $headerValue = RequestUtils::getHeader($request, $headerName);
  4382. $parts = explode(' ', trim($headerValue), 2);
  4383. if (count($parts) != 2) {
  4384. return '';
  4385. }
  4386. if ($parts[0] != 'Bearer') {
  4387. return '';
  4388. }
  4389. return $parts[1];
  4390. }
  4391. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4392. {
  4393. if (session_status() == PHP_SESSION_NONE) {
  4394. session_start();
  4395. }
  4396. $token = $this->getAuthorizationToken($request);
  4397. if ($token) {
  4398. $claims = $this->getClaims($token);
  4399. $_SESSION['claims'] = $claims;
  4400. if (empty($claims)) {
  4401. return $this->responder->error(ErrorCode::AUTHENTICATION_FAILED, 'JWT');
  4402. }
  4403. if (!headers_sent()) {
  4404. session_regenerate_id();
  4405. }
  4406. }
  4407. if (empty($_SESSION['claims'])) {
  4408. $authenticationMode = $this->getProperty('mode', 'required');
  4409. if ($authenticationMode == 'required') {
  4410. return $this->responder->error(ErrorCode::AUTHENTICATION_REQUIRED, '');
  4411. }
  4412. }
  4413. return $next->handle($request);
  4414. }
  4415. }
  4416. // file: src/Tqdev/PhpCrudApi/Middleware/MultiTenancyMiddleware.php
  4417. class MultiTenancyMiddleware extends Middleware
  4418. {
  4419. private $reflection;
  4420. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4421. {
  4422. parent::__construct($router, $responder, $properties);
  4423. $this->reflection = $reflection;
  4424. }
  4425. private function getCondition(string $tableName, array $pairs): Condition
  4426. {
  4427. $condition = new NoCondition();
  4428. $table = $this->reflection->getTable($tableName);
  4429. foreach ($pairs as $k => $v) {
  4430. $condition = $condition->_and(new ColumnCondition($table->getColumn($k), 'eq', $v));
  4431. }
  4432. return $condition;
  4433. }
  4434. private function getPairs($handler, string $operation, string $tableName): array
  4435. {
  4436. $result = array();
  4437. $pairs = call_user_func($handler, $operation, $tableName);
  4438. $table = $this->reflection->getTable($tableName);
  4439. foreach ($pairs as $k => $v) {
  4440. if ($table->hasColumn($k)) {
  4441. $result[$k] = $v;
  4442. }
  4443. }
  4444. return $result;
  4445. }
  4446. private function handleRecord(ServerRequestInterface $request, string $operation, array $pairs): ServerRequestInterface
  4447. {
  4448. $record = $request->getParsedBody();
  4449. if ($record === null) {
  4450. return $request;
  4451. }
  4452. $multi = is_array($record);
  4453. $records = $multi ? $record : [$record];
  4454. foreach ($records as &$record) {
  4455. foreach ($pairs as $column => $value) {
  4456. if ($operation == 'create') {
  4457. $record->$column = $value;
  4458. } else {
  4459. if (isset($record->$column)) {
  4460. unset($record->$column);
  4461. }
  4462. }
  4463. }
  4464. }
  4465. return $request->withParsedBody($multi ? $records : $records[0]);
  4466. }
  4467. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4468. {
  4469. $handler = $this->getProperty('handler', '');
  4470. if ($handler !== '') {
  4471. $path = RequestUtils::getPathSegment($request, 1);
  4472. if ($path == 'records') {
  4473. $operation = RequestUtils::getOperation($request);
  4474. $tableNames = RequestUtils::getTableNames($request, $this->reflection);
  4475. foreach ($tableNames as $i => $tableName) {
  4476. if (!$this->reflection->hasTable($tableName)) {
  4477. continue;
  4478. }
  4479. $pairs = $this->getPairs($handler, $operation, $tableName);
  4480. if ($i == 0) {
  4481. if (in_array($operation, ['create', 'update', 'increment'])) {
  4482. $request = $this->handleRecord($request, $operation, $pairs);
  4483. }
  4484. }
  4485. $condition = $this->getCondition($tableName, $pairs);
  4486. VariableStore::set("multiTenancy.conditions.$tableName", $condition);
  4487. }
  4488. }
  4489. }
  4490. return $next->handle($request);
  4491. }
  4492. }
  4493. // file: src/Tqdev/PhpCrudApi/Middleware/PageLimitsMiddleware.php
  4494. class PageLimitsMiddleware extends Middleware
  4495. {
  4496. private $reflection;
  4497. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4498. {
  4499. parent::__construct($router, $responder, $properties);
  4500. $this->reflection = $reflection;
  4501. }
  4502. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4503. {
  4504. $operation = RequestUtils::getOperation($request);
  4505. if ($operation == 'list') {
  4506. $params = RequestUtils::getParams($request);
  4507. $maxPage = (int) $this->getProperty('pages', '100');
  4508. if (isset($params['page']) && $params['page'] && $maxPage > 0) {
  4509. if (strpos($params['page'][0], ',') === false) {
  4510. $page = $params['page'][0];
  4511. } else {
  4512. list($page, $size) = explode(',', $params['page'][0], 2);
  4513. }
  4514. if ($page > $maxPage) {
  4515. return $this->responder->error(ErrorCode::PAGINATION_FORBIDDEN, '');
  4516. }
  4517. }
  4518. $maxSize = (int) $this->getProperty('records', '1000');
  4519. if (!isset($params['size']) || !$params['size'] && $maxSize > 0) {
  4520. $params['size'] = array($maxSize);
  4521. } else {
  4522. $params['size'] = array(min($params['size'][0], $maxSize));
  4523. }
  4524. $request = RequestUtils::setParams($request, $params);
  4525. }
  4526. return $next->handle($request);
  4527. }
  4528. }
  4529. // file: src/Tqdev/PhpCrudApi/Middleware/SanitationMiddleware.php
  4530. class SanitationMiddleware extends Middleware
  4531. {
  4532. private $reflection;
  4533. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4534. {
  4535. parent::__construct($router, $responder, $properties);
  4536. $this->reflection = $reflection;
  4537. }
  4538. private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: object */
  4539. {
  4540. $context = (array) $record;
  4541. $tableName = $table->getName();
  4542. foreach ($context as $columnName => &$value) {
  4543. if ($table->hasColumn($columnName)) {
  4544. $column = $table->getColumn($columnName);
  4545. $value = call_user_func($handler, $operation, $tableName, $column->serialize(), $value);
  4546. }
  4547. }
  4548. return (object) $context;
  4549. }
  4550. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4551. {
  4552. $operation = RequestUtils::getOperation($request);
  4553. if (in_array($operation, ['create', 'update', 'increment'])) {
  4554. $tableName = RequestUtils::getPathSegment($request, 2);
  4555. if ($this->reflection->hasTable($tableName)) {
  4556. $record = $request->getParsedBody();
  4557. if ($record !== null) {
  4558. $handler = $this->getProperty('handler', '');
  4559. if ($handler !== '') {
  4560. $table = $this->reflection->getTable($tableName);
  4561. if (is_array($record)) {
  4562. foreach ($record as &$r) {
  4563. $r = $this->callHandler($handler, $r, $operation, $table);
  4564. }
  4565. } else {
  4566. $record = $this->callHandler($handler, $record, $operation, $table);
  4567. }
  4568. $request = $request->withParsedBody($record);
  4569. }
  4570. }
  4571. }
  4572. }
  4573. return $next->handle($request);
  4574. }
  4575. }
  4576. // file: src/Tqdev/PhpCrudApi/Middleware/ValidationMiddleware.php
  4577. class ValidationMiddleware extends Middleware
  4578. {
  4579. private $reflection;
  4580. public function __construct(Router $router, Responder $responder, array $properties, ReflectionService $reflection)
  4581. {
  4582. parent::__construct($router, $responder, $properties);
  4583. $this->reflection = $reflection;
  4584. }
  4585. private function callHandler($handler, $record, string $operation, ReflectedTable $table) /*: ResponseInterface?*/
  4586. {
  4587. $context = (array) $record;
  4588. $details = array();
  4589. $tableName = $table->getName();
  4590. foreach ($context as $columnName => $value) {
  4591. if ($table->hasColumn($columnName)) {
  4592. $column = $table->getColumn($columnName);
  4593. $valid = call_user_func($handler, $operation, $tableName, $column->serialize(), $value, $context);
  4594. if ($valid !== true && $valid !== '') {
  4595. $details[$columnName] = $valid;
  4596. }
  4597. }
  4598. }
  4599. if (count($details) > 0) {
  4600. return $this->responder->error(ErrorCode::INPUT_VALIDATION_FAILED, $tableName, $details);
  4601. }
  4602. return null;
  4603. }
  4604. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4605. {
  4606. $operation = RequestUtils::getOperation($request);
  4607. if (in_array($operation, ['create', 'update', 'increment'])) {
  4608. $tableName = RequestUtils::getPathSegment($request, 2);
  4609. if ($this->reflection->hasTable($tableName)) {
  4610. $record = $request->getParsedBody();
  4611. if ($record !== null) {
  4612. $handler = $this->getProperty('handler', '');
  4613. if ($handler !== '') {
  4614. $table = $this->reflection->getTable($tableName);
  4615. if (is_array($record)) {
  4616. foreach ($record as $r) {
  4617. $response = $this->callHandler($handler, $r, $operation, $table);
  4618. if ($response !== null) {
  4619. return $response;
  4620. }
  4621. }
  4622. } else {
  4623. $response = $this->callHandler($handler, $record, $operation, $table);
  4624. if ($response !== null) {
  4625. return $response;
  4626. }
  4627. }
  4628. }
  4629. }
  4630. }
  4631. }
  4632. return $next->handle($request);
  4633. }
  4634. }
  4635. // file: src/Tqdev/PhpCrudApi/Middleware/XsrfMiddleware.php
  4636. class XsrfMiddleware extends Middleware
  4637. {
  4638. private function getToken(): string
  4639. {
  4640. $cookieName = $this->getProperty('cookieName', 'XSRF-TOKEN');
  4641. if (isset($_COOKIE[$cookieName])) {
  4642. $token = $_COOKIE[$cookieName];
  4643. } else {
  4644. $secure = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on';
  4645. $token = bin2hex(random_bytes(8));
  4646. if (!headers_sent()) {
  4647. setcookie($cookieName, $token, 0, '', '', $secure);
  4648. }
  4649. }
  4650. return $token;
  4651. }
  4652. public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
  4653. {
  4654. $token = $this->getToken();
  4655. $method = $request->getMethod();
  4656. $excludeMethods = $this->getArrayProperty('excludeMethods', 'OPTIONS,GET');
  4657. if (!in_array($method, $excludeMethods)) {
  4658. $headerName = $this->getProperty('headerName', 'X-XSRF-TOKEN');
  4659. if ($token != $request->getHeader($headerName)) {
  4660. return $this->responder->error(ErrorCode::BAD_OR_MISSING_XSRF_TOKEN, '');
  4661. }
  4662. }
  4663. return $next->handle($request);
  4664. }
  4665. }
  4666. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiBuilder.php
  4667. class OpenApiBuilder
  4668. {
  4669. private $openapi;
  4670. private $reflection;
  4671. private $operations = [
  4672. 'list' => 'get',
  4673. 'create' => 'post',
  4674. 'read' => 'get',
  4675. 'update' => 'put',
  4676. 'delete' => 'delete',
  4677. 'increment' => 'patch',
  4678. ];
  4679. private $types = [
  4680. 'integer' => ['type' => 'integer', 'format' => 'int32'],
  4681. 'bigint' => ['type' => 'integer', 'format' => 'int64'],
  4682. 'varchar' => ['type' => 'string'],
  4683. 'clob' => ['type' => 'string'],
  4684. 'varbinary' => ['type' => 'string', 'format' => 'byte'],
  4685. 'blob' => ['type' => 'string', 'format' => 'byte'],
  4686. 'decimal' => ['type' => 'string'],
  4687. 'float' => ['type' => 'number', 'format' => 'float'],
  4688. 'double' => ['type' => 'number', 'format' => 'double'],
  4689. 'date' => ['type' => 'string', 'format' => 'date'],
  4690. 'time' => ['type' => 'string', 'format' => 'date-time'],
  4691. 'timestamp' => ['type' => 'string', 'format' => 'date-time'],
  4692. 'geometry' => ['type' => 'string'],
  4693. 'boolean' => ['type' => 'boolean'],
  4694. ];
  4695. public function __construct(ReflectionService $reflection, $base)
  4696. {
  4697. $this->reflection = $reflection;
  4698. $this->openapi = new OpenApiDefinition($base);
  4699. }
  4700. private function getServerUrl(): string
  4701. {
  4702. $protocol = @$_SERVER['HTTP_X_FORWARDED_PROTO'] ?: @$_SERVER['REQUEST_SCHEME'] ?: ((isset($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] == "on") ? "https" : "http");
  4703. $port = @intval($_SERVER['HTTP_X_FORWARDED_PORT']) ?: @intval($_SERVER["SERVER_PORT"]) ?: (($protocol === 'https') ? 443 : 80);
  4704. $host = @explode(":", $_SERVER['HTTP_HOST'])[0] ?: @$_SERVER['SERVER_NAME'] ?: @$_SERVER['SERVER_ADDR'];
  4705. $port = ($protocol === 'https' && $port === 443) || ($protocol === 'http' && $port === 80) ? '' : ':' . $port;
  4706. $path = @trim(substr($_SERVER['REQUEST_URI'], 0, strpos($_SERVER['REQUEST_URI'], '/openapi')), '/');
  4707. return sprintf('%s://%s%s/%s', $protocol, $host, $port, $path);
  4708. }
  4709. private function getAllTableReferences(): array
  4710. {
  4711. $tableReferences = array();
  4712. foreach ($this->reflection->getTableNames() as $tableName) {
  4713. $table = $this->reflection->getTable($tableName);
  4714. foreach ($table->getColumnNames() as $columnName) {
  4715. $column = $table->getColumn($columnName);
  4716. $referencedTableName = $column->getFk();
  4717. if ($referencedTableName) {
  4718. if (!isset($tableReferences[$referencedTableName])) {
  4719. $tableReferences[$referencedTableName] = array();
  4720. }
  4721. $tableReferences[$referencedTableName][] = "$tableName.$columnName";
  4722. }
  4723. }
  4724. }
  4725. return $tableReferences;
  4726. }
  4727. public function build(): OpenApiDefinition
  4728. {
  4729. $this->openapi->set("openapi", "3.0.0");
  4730. if (!$this->openapi->has("servers") && isset($_SERVER['REQUEST_URI'])) {
  4731. $this->openapi->set("servers|0|url", $this->getServerUrl());
  4732. }
  4733. $tableNames = $this->reflection->getTableNames();
  4734. foreach ($tableNames as $tableName) {
  4735. $this->setPath($tableName);
  4736. }
  4737. $this->openapi->set("components|responses|pk_integer|description", "inserted primary key value (integer)");
  4738. $this->openapi->set("components|responses|pk_integer|content|application/json|schema|type", "integer");
  4739. $this->openapi->set("components|responses|pk_integer|content|application/json|schema|format", "int64");
  4740. $this->openapi->set("components|responses|pk_string|description", "inserted primary key value (string)");
  4741. $this->openapi->set("components|responses|pk_string|content|application/json|schema|type", "string");
  4742. $this->openapi->set("components|responses|pk_string|content|application/json|schema|format", "uuid");
  4743. $this->openapi->set("components|responses|rows_affected|description", "number of rows affected (integer)");
  4744. $this->openapi->set("components|responses|rows_affected|content|application/json|schema|type", "integer");
  4745. $this->openapi->set("components|responses|rows_affected|content|application/json|schema|format", "int64");
  4746. $tableReferences = $this->getAllTableReferences();
  4747. foreach ($tableNames as $tableName) {
  4748. $references = isset($tableReferences[$tableName]) ? $tableReferences[$tableName] : array();
  4749. $this->setComponentSchema($tableName, $references);
  4750. $this->setComponentResponse($tableName);
  4751. $this->setComponentRequestBody($tableName);
  4752. }
  4753. $this->setComponentParameters();
  4754. foreach ($tableNames as $index => $tableName) {
  4755. $this->setTag($index, $tableName);
  4756. }
  4757. return $this->openapi;
  4758. }
  4759. private function isOperationOnTableAllowed(string $operation, string $tableName): bool
  4760. {
  4761. $tableHandler = VariableStore::get('authorization.tableHandler');
  4762. if (!$tableHandler) {
  4763. return true;
  4764. }
  4765. return (bool) call_user_func($tableHandler, $operation, $tableName);
  4766. }
  4767. private function isOperationOnColumnAllowed(string $operation, string $tableName, string $columnName): bool
  4768. {
  4769. $columnHandler = VariableStore::get('authorization.columnHandler');
  4770. if (!$columnHandler) {
  4771. return true;
  4772. }
  4773. return (bool) call_user_func($columnHandler, $operation, $tableName, $columnName);
  4774. }
  4775. private function setPath(string $tableName) /*: void*/
  4776. {
  4777. $table = $this->reflection->getTable($tableName);
  4778. $type = $table->getType();
  4779. $pk = $table->getPk();
  4780. $pkName = $pk ? $pk->getName() : '';
  4781. foreach ($this->operations as $operation => $method) {
  4782. if (!$pkName && $operation != 'list') {
  4783. continue;
  4784. }
  4785. if ($type != 'table' && $operation != 'list') {
  4786. continue;
  4787. }
  4788. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4789. continue;
  4790. }
  4791. $parameters = [];
  4792. if (in_array($operation, ['list', 'create'])) {
  4793. $path = sprintf('/records/%s', $tableName);
  4794. if ($operation == 'list') {
  4795. $parameters = ['filter', 'include', 'exclude', 'order', 'size', 'page', 'join'];
  4796. }
  4797. } else {
  4798. $path = sprintf('/records/%s/{%s}', $tableName, $pkName);
  4799. if ($operation == 'read') {
  4800. $parameters = ['pk', 'include', 'exclude', 'join'];
  4801. } else {
  4802. $parameters = ['pk'];
  4803. }
  4804. }
  4805. foreach ($parameters as $p => $parameter) {
  4806. $this->openapi->set("paths|$path|$method|parameters|$p|\$ref", "#/components/parameters/$parameter");
  4807. }
  4808. if (in_array($operation, ['create', 'update', 'increment'])) {
  4809. $this->openapi->set("paths|$path|$method|requestBody|\$ref", "#/components/requestBodies/$operation-" . urlencode($tableName));
  4810. }
  4811. $this->openapi->set("paths|$path|$method|tags|0", "$tableName");
  4812. $this->openapi->set("paths|$path|$method|description", "$operation $tableName");
  4813. switch ($operation) {
  4814. case 'list':
  4815. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . urlencode($tableName));
  4816. break;
  4817. case 'create':
  4818. if ($pk->getType() == 'integer') {
  4819. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_integer");
  4820. } else {
  4821. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/pk_string");
  4822. }
  4823. break;
  4824. case 'read':
  4825. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/$operation-" . urlencode($tableName));
  4826. break;
  4827. case 'update':
  4828. case 'delete':
  4829. case 'increment':
  4830. $this->openapi->set("paths|$path|$method|responses|200|\$ref", "#/components/responses/rows_affected");
  4831. break;
  4832. }
  4833. }
  4834. }
  4835. private function setComponentSchema(string $tableName, array $references) /*: void*/
  4836. {
  4837. $table = $this->reflection->getTable($tableName);
  4838. $type = $table->getType();
  4839. $pk = $table->getPk();
  4840. $pkName = $pk ? $pk->getName() : '';
  4841. foreach ($this->operations as $operation => $method) {
  4842. if (!$pkName && $operation != 'list') {
  4843. continue;
  4844. }
  4845. if ($type != 'table' && $operation != 'list') {
  4846. continue;
  4847. }
  4848. if ($operation == 'delete') {
  4849. continue;
  4850. }
  4851. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4852. continue;
  4853. }
  4854. if ($operation == 'list') {
  4855. $this->openapi->set("components|schemas|$operation-$tableName|type", "object");
  4856. $this->openapi->set("components|schemas|$operation-$tableName|properties|results|type", "integer");
  4857. $this->openapi->set("components|schemas|$operation-$tableName|properties|results|format", "int64");
  4858. $this->openapi->set("components|schemas|$operation-$tableName|properties|records|type", "array");
  4859. $prefix = "components|schemas|$operation-$tableName|properties|records|items";
  4860. } else {
  4861. $prefix = "components|schemas|$operation-$tableName";
  4862. }
  4863. $this->openapi->set("$prefix|type", "object");
  4864. foreach ($table->getColumnNames() as $columnName) {
  4865. if (!$this->isOperationOnColumnAllowed($operation, $tableName, $columnName)) {
  4866. continue;
  4867. }
  4868. $column = $table->getColumn($columnName);
  4869. $properties = $this->types[$column->getType()];
  4870. foreach ($properties as $key => $value) {
  4871. $this->openapi->set("$prefix|properties|$columnName|$key", $value);
  4872. }
  4873. if ($column->getPk()) {
  4874. $this->openapi->set("$prefix|properties|$columnName|x-primary-key", true);
  4875. $this->openapi->set("$prefix|properties|$columnName|x-referenced", $references);
  4876. }
  4877. $fk = $column->getFk();
  4878. if ($fk) {
  4879. $this->openapi->set("$prefix|properties|$columnName|x-references", $fk);
  4880. }
  4881. }
  4882. }
  4883. }
  4884. private function setComponentResponse(string $tableName) /*: void*/
  4885. {
  4886. $table = $this->reflection->getTable($tableName);
  4887. $type = $table->getType();
  4888. $pk = $table->getPk();
  4889. $pkName = $pk ? $pk->getName() : '';
  4890. foreach (['list', 'read'] as $operation) {
  4891. if (!$pkName && $operation != 'list') {
  4892. continue;
  4893. }
  4894. if ($type != 'table' && $operation != 'list') {
  4895. continue;
  4896. }
  4897. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4898. continue;
  4899. }
  4900. if ($operation == 'list') {
  4901. $this->openapi->set("components|responses|$operation-$tableName|description", "list of $tableName records");
  4902. } else {
  4903. $this->openapi->set("components|responses|$operation-$tableName|description", "single $tableName record");
  4904. }
  4905. $this->openapi->set("components|responses|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . urlencode($tableName));
  4906. }
  4907. }
  4908. private function setComponentRequestBody(string $tableName) /*: void*/
  4909. {
  4910. $table = $this->reflection->getTable($tableName);
  4911. $type = $table->getType();
  4912. $pk = $table->getPk();
  4913. $pkName = $pk ? $pk->getName() : '';
  4914. if ($pkName && $type == 'table') {
  4915. foreach (['create', 'update', 'increment'] as $operation) {
  4916. if (!$this->isOperationOnTableAllowed($operation, $tableName)) {
  4917. continue;
  4918. }
  4919. $this->openapi->set("components|requestBodies|$operation-$tableName|description", "single $tableName record");
  4920. $this->openapi->set("components|requestBodies|$operation-$tableName|content|application/json|schema|\$ref", "#/components/schemas/$operation-" . urlencode($tableName));
  4921. }
  4922. }
  4923. }
  4924. private function setComponentParameters() /*: void*/
  4925. {
  4926. $this->openapi->set("components|parameters|pk|name", "id");
  4927. $this->openapi->set("components|parameters|pk|in", "path");
  4928. $this->openapi->set("components|parameters|pk|schema|type", "string");
  4929. $this->openapi->set("components|parameters|pk|description", "primary key value");
  4930. $this->openapi->set("components|parameters|pk|required", true);
  4931. $this->openapi->set("components|parameters|filter|name", "filter");
  4932. $this->openapi->set("components|parameters|filter|in", "query");
  4933. $this->openapi->set("components|parameters|filter|schema|type", "array");
  4934. $this->openapi->set("components|parameters|filter|schema|items|type", "string");
  4935. $this->openapi->set("components|parameters|filter|description", "Filters to be applied. Each filter consists of a column, an operator and a value (comma separated). Example: id,eq,1");
  4936. $this->openapi->set("components|parameters|filter|required", false);
  4937. $this->openapi->set("components|parameters|include|name", "include");
  4938. $this->openapi->set("components|parameters|include|in", "query");
  4939. $this->openapi->set("components|parameters|include|schema|type", "string");
  4940. $this->openapi->set("components|parameters|include|description", "Columns you want to include in the output (comma separated). Example: posts.*,categories.name");
  4941. $this->openapi->set("components|parameters|include|required", false);
  4942. $this->openapi->set("components|parameters|exclude|name", "exclude");
  4943. $this->openapi->set("components|parameters|exclude|in", "query");
  4944. $this->openapi->set("components|parameters|exclude|schema|type", "string");
  4945. $this->openapi->set("components|parameters|exclude|description", "Columns you want to exclude from the output (comma separated). Example: posts.content");
  4946. $this->openapi->set("components|parameters|exclude|required", false);
  4947. $this->openapi->set("components|parameters|order|name", "order");
  4948. $this->openapi->set("components|parameters|order|in", "query");
  4949. $this->openapi->set("components|parameters|order|schema|type", "array");
  4950. $this->openapi->set("components|parameters|order|schema|items|type", "string");
  4951. $this->openapi->set("components|parameters|order|description", "Column you want to sort on and the sort direction (comma separated). Example: id,desc");
  4952. $this->openapi->set("components|parameters|order|required", false);
  4953. $this->openapi->set("components|parameters|size|name", "size");
  4954. $this->openapi->set("components|parameters|size|in", "query");
  4955. $this->openapi->set("components|parameters|size|schema|type", "string");
  4956. $this->openapi->set("components|parameters|size|description", "Maximum number of results (for top lists). Example: 10");
  4957. $this->openapi->set("components|parameters|size|required", false);
  4958. $this->openapi->set("components|parameters|page|name", "page");
  4959. $this->openapi->set("components|parameters|page|in", "query");
  4960. $this->openapi->set("components|parameters|page|schema|type", "string");
  4961. $this->openapi->set("components|parameters|page|description", "Page number and page size (comma separated). Example: 1,10");
  4962. $this->openapi->set("components|parameters|page|required", false);
  4963. $this->openapi->set("components|parameters|join|name", "join");
  4964. $this->openapi->set("components|parameters|join|in", "query");
  4965. $this->openapi->set("components|parameters|join|schema|type", "array");
  4966. $this->openapi->set("components|parameters|join|schema|items|type", "string");
  4967. $this->openapi->set("components|parameters|join|description", "Paths (comma separated) to related entities that you want to include. Example: comments,users");
  4968. $this->openapi->set("components|parameters|join|required", false);
  4969. }
  4970. private function setTag(int $index, string $tableName) /*: void*/
  4971. {
  4972. $this->openapi->set("tags|$index|name", "$tableName");
  4973. $this->openapi->set("tags|$index|description", "$tableName operations");
  4974. }
  4975. }
  4976. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiDefinition.php
  4977. class OpenApiDefinition implements \JsonSerializable
  4978. {
  4979. private $root;
  4980. public function __construct($base)
  4981. {
  4982. $this->root = $base;
  4983. }
  4984. public function set(string $path, $value) /*: void*/
  4985. {
  4986. $parts = explode('|', trim($path, '|'));
  4987. $current = &$this->root;
  4988. while (count($parts) > 0) {
  4989. $part = array_shift($parts);
  4990. if (!isset($current[$part])) {
  4991. $current[$part] = [];
  4992. }
  4993. $current = &$current[$part];
  4994. }
  4995. $current = $value;
  4996. }
  4997. public function has(string $path): bool
  4998. {
  4999. $parts = explode('|', trim($path, '|'));
  5000. $current = &$this->root;
  5001. while (count($parts) > 0) {
  5002. $part = array_shift($parts);
  5003. if (!isset($current[$part])) {
  5004. return false;
  5005. }
  5006. $current = &$current[$part];
  5007. }
  5008. return true;
  5009. }
  5010. public function jsonSerialize()
  5011. {
  5012. return $this->root;
  5013. }
  5014. }
  5015. // file: src/Tqdev/PhpCrudApi/OpenApi/OpenApiService.php
  5016. class OpenApiService
  5017. {
  5018. private $builder;
  5019. public function __construct(ReflectionService $reflection, array $base)
  5020. {
  5021. $this->builder = new OpenApiBuilder($reflection, $base);
  5022. }
  5023. public function get(): OpenApiDefinition
  5024. {
  5025. return $this->builder->build();
  5026. }
  5027. }
  5028. // file: src/Tqdev/PhpCrudApi/Record/Condition/AndCondition.php
  5029. class AndCondition extends Condition
  5030. {
  5031. private $conditions;
  5032. public function __construct(Condition $condition1, Condition $condition2)
  5033. {
  5034. $this->conditions = [$condition1, $condition2];
  5035. }
  5036. public function _and(Condition $condition): Condition
  5037. {
  5038. if ($condition instanceof NoCondition) {
  5039. return $this;
  5040. }
  5041. $this->conditions[] = $condition;
  5042. return $this;
  5043. }
  5044. public function getConditions(): array
  5045. {
  5046. return $this->conditions;
  5047. }
  5048. public static function fromArray(array $conditions): Condition
  5049. {
  5050. $condition = new NoCondition();
  5051. foreach ($conditions as $c) {
  5052. $condition = $condition->_and($c);
  5053. }
  5054. return $condition;
  5055. }
  5056. }
  5057. // file: src/Tqdev/PhpCrudApi/Record/Condition/ColumnCondition.php
  5058. class ColumnCondition extends Condition
  5059. {
  5060. private $column;
  5061. private $operator;
  5062. private $value;
  5063. public function __construct(ReflectedColumn $column, string $operator, string $value)
  5064. {
  5065. $this->column = $column;
  5066. $this->operator = $operator;
  5067. $this->value = $value;
  5068. }
  5069. public function getColumn(): ReflectedColumn
  5070. {
  5071. return $this->column;
  5072. }
  5073. public function getOperator(): string
  5074. {
  5075. return $this->operator;
  5076. }
  5077. public function getValue(): string
  5078. {
  5079. return $this->value;
  5080. }
  5081. }
  5082. // file: src/Tqdev/PhpCrudApi/Record/Condition/Condition.php
  5083. abstract class Condition
  5084. {
  5085. public function _and(Condition $condition): Condition
  5086. {
  5087. if ($condition instanceof NoCondition) {
  5088. return $this;
  5089. }
  5090. return new AndCondition($this, $condition);
  5091. }
  5092. public function _or(Condition $condition): Condition
  5093. {
  5094. if ($condition instanceof NoCondition) {
  5095. return $this;
  5096. }
  5097. return new OrCondition($this, $condition);
  5098. }
  5099. public function _not(): Condition
  5100. {
  5101. return new NotCondition($this);
  5102. }
  5103. public static function fromString(ReflectedTable $table, string $value): Condition
  5104. {
  5105. $condition = new NoCondition();
  5106. $parts = explode(',', $value, 3);
  5107. if (count($parts) < 2) {
  5108. return $condition;
  5109. }
  5110. if (count($parts) < 3) {
  5111. $parts[2] = '';
  5112. }
  5113. $field = $table->getColumn($parts[0]);
  5114. $command = $parts[1];
  5115. $negate = false;
  5116. $spatial = false;
  5117. if (strlen($command) > 2) {
  5118. if (substr($command, 0, 1) == 'n') {
  5119. $negate = true;
  5120. $command = substr($command, 1);
  5121. }
  5122. if (substr($command, 0, 1) == 's') {
  5123. $spatial = true;
  5124. $command = substr($command, 1);
  5125. }
  5126. }
  5127. if ($spatial) {
  5128. if (in_array($command, ['co', 'cr', 'di', 'eq', 'in', 'ov', 'to', 'wi', 'ic', 'is', 'iv'])) {
  5129. $condition = new SpatialCondition($field, $command, $parts[2]);
  5130. }
  5131. } else {
  5132. if (in_array($command, ['cs', 'sw', 'ew', 'eq', 'lt', 'le', 'ge', 'gt', 'bt', 'in', 'is'])) {
  5133. $condition = new ColumnCondition($field, $command, $parts[2]);
  5134. }
  5135. }
  5136. if ($negate) {
  5137. $condition = $condition->_not();
  5138. }
  5139. return $condition;
  5140. }
  5141. }
  5142. // file: src/Tqdev/PhpCrudApi/Record/Condition/NoCondition.php
  5143. class NoCondition extends Condition
  5144. {
  5145. public function _and(Condition $condition): Condition
  5146. {
  5147. return $condition;
  5148. }
  5149. public function _or(Condition $condition): Condition
  5150. {
  5151. return $condition;
  5152. }
  5153. public function _not(): Condition
  5154. {
  5155. return $this;
  5156. }
  5157. }
  5158. // file: src/Tqdev/PhpCrudApi/Record/Condition/NotCondition.php
  5159. class NotCondition extends Condition
  5160. {
  5161. private $condition;
  5162. public function __construct(Condition $condition)
  5163. {
  5164. $this->condition = $condition;
  5165. }
  5166. public function getCondition(): Condition
  5167. {
  5168. return $this->condition;
  5169. }
  5170. }
  5171. // file: src/Tqdev/PhpCrudApi/Record/Condition/OrCondition.php
  5172. class OrCondition extends Condition
  5173. {
  5174. private $conditions;
  5175. public function __construct(Condition $condition1, Condition $condition2)
  5176. {
  5177. $this->conditions = [$condition1, $condition2];
  5178. }
  5179. public function _or(Condition $condition): Condition
  5180. {
  5181. if ($condition instanceof NoCondition) {
  5182. return $this;
  5183. }
  5184. $this->conditions[] = $condition;
  5185. return $this;
  5186. }
  5187. public function getConditions(): array
  5188. {
  5189. return $this->conditions;
  5190. }
  5191. public static function fromArray(array $conditions): Condition
  5192. {
  5193. $condition = new NoCondition();
  5194. foreach ($conditions as $c) {
  5195. $condition = $condition->_or($c);
  5196. }
  5197. return $condition;
  5198. }
  5199. }
  5200. // file: src/Tqdev/PhpCrudApi/Record/Condition/SpatialCondition.php
  5201. class SpatialCondition extends ColumnCondition
  5202. {
  5203. }
  5204. // file: src/Tqdev/PhpCrudApi/Record/Document/ErrorDocument.php
  5205. class ErrorDocument implements \JsonSerializable
  5206. {
  5207. public $code;
  5208. public $message;
  5209. public $details;
  5210. public function __construct(ErrorCode $errorCode, string $argument, $details)
  5211. {
  5212. $this->code = $errorCode->getCode();
  5213. $this->message = $errorCode->getMessage($argument);
  5214. $this->details = $details;
  5215. }
  5216. public function getCode(): int
  5217. {
  5218. return $this->code;
  5219. }
  5220. public function getMessage(): string
  5221. {
  5222. return $this->message;
  5223. }
  5224. public function serialize()
  5225. {
  5226. return [
  5227. 'code' => $this->code,
  5228. 'message' => $this->message,
  5229. 'details' => $this->details,
  5230. ];
  5231. }
  5232. public function jsonSerialize()
  5233. {
  5234. return array_filter($this->serialize());
  5235. }
  5236. }
  5237. // file: src/Tqdev/PhpCrudApi/Record/Document/ListDocument.php
  5238. class ListDocument implements \JsonSerializable
  5239. {
  5240. private $records;
  5241. private $results;
  5242. public function __construct(array $records, int $results)
  5243. {
  5244. $this->records = $records;
  5245. $this->results = $results;
  5246. }
  5247. public function getRecords(): array
  5248. {
  5249. return $this->records;
  5250. }
  5251. public function getResults(): int
  5252. {
  5253. return $this->results;
  5254. }
  5255. public function serialize()
  5256. {
  5257. return [
  5258. 'records' => $this->records,
  5259. 'results' => $this->results,
  5260. ];
  5261. }
  5262. public function jsonSerialize()
  5263. {
  5264. return array_filter($this->serialize(), function ($v) {
  5265. return $v !== 0;
  5266. });
  5267. }
  5268. }
  5269. // file: src/Tqdev/PhpCrudApi/Record/ColumnIncluder.php
  5270. class ColumnIncluder
  5271. {
  5272. private function isMandatory(string $tableName, string $columnName, array $params): bool
  5273. {
  5274. return isset($params['mandatory']) && in_array($tableName . "." . $columnName, $params['mandatory']);
  5275. }
  5276. private function select(string $tableName, bool $primaryTable, array $params, string $paramName,
  5277. array $columnNames, bool $include): array{
  5278. if (!isset($params[$paramName])) {
  5279. return $columnNames;
  5280. }
  5281. $columns = array();
  5282. foreach (explode(',', $params[$paramName][0]) as $columnName) {
  5283. $columns[$columnName] = true;
  5284. }
  5285. $result = array();
  5286. foreach ($columnNames as $columnName) {
  5287. $match = isset($columns['*.*']);
  5288. if (!$match) {
  5289. $match = isset($columns[$tableName . '.*']) || isset($columns[$tableName . '.' . $columnName]);
  5290. }
  5291. if ($primaryTable && !$match) {
  5292. $match = isset($columns['*']) || isset($columns[$columnName]);
  5293. }
  5294. if ($match) {
  5295. if ($include || $this->isMandatory($tableName, $columnName, $params)) {
  5296. $result[] = $columnName;
  5297. }
  5298. } else {
  5299. if (!$include || $this->isMandatory($tableName, $columnName, $params)) {
  5300. $result[] = $columnName;
  5301. }
  5302. }
  5303. }
  5304. return $result;
  5305. }
  5306. public function getNames(ReflectedTable $table, bool $primaryTable, array $params): array
  5307. {
  5308. $tableName = $table->getName();
  5309. $results = $table->getColumnNames();
  5310. $results = $this->select($tableName, $primaryTable, $params, 'include', $results, true);
  5311. $results = $this->select($tableName, $primaryTable, $params, 'exclude', $results, false);
  5312. return $results;
  5313. }
  5314. public function getValues(ReflectedTable $table, bool $primaryTable, /* object */ $record, array $params): array
  5315. {
  5316. $results = array();
  5317. $columnNames = $this->getNames($table, $primaryTable, $params);
  5318. foreach ($columnNames as $columnName) {
  5319. if (property_exists($record, $columnName)) {
  5320. $results[$columnName] = $record->$columnName;
  5321. }
  5322. }
  5323. return $results;
  5324. }
  5325. }
  5326. // file: src/Tqdev/PhpCrudApi/Record/ErrorCode.php
  5327. class ErrorCode
  5328. {
  5329. private $code;
  5330. private $message;
  5331. private $status;
  5332. const ERROR_NOT_FOUND = 9999;
  5333. const ROUTE_NOT_FOUND = 1000;
  5334. const TABLE_NOT_FOUND = 1001;
  5335. const ARGUMENT_COUNT_MISMATCH = 1002;
  5336. const RECORD_NOT_FOUND = 1003;
  5337. const ORIGIN_FORBIDDEN = 1004;
  5338. const COLUMN_NOT_FOUND = 1005;
  5339. const TABLE_ALREADY_EXISTS = 1006;
  5340. const COLUMN_ALREADY_EXISTS = 1007;
  5341. const HTTP_MESSAGE_NOT_READABLE = 1008;
  5342. const DUPLICATE_KEY_EXCEPTION = 1009;
  5343. const DATA_INTEGRITY_VIOLATION = 1010;
  5344. const AUTHENTICATION_REQUIRED = 1011;
  5345. const AUTHENTICATION_FAILED = 1012;
  5346. const INPUT_VALIDATION_FAILED = 1013;
  5347. const OPERATION_FORBIDDEN = 1014;
  5348. const OPERATION_NOT_SUPPORTED = 1015;
  5349. const TEMPORARY_OR_PERMANENTLY_BLOCKED = 1016;
  5350. const BAD_OR_MISSING_XSRF_TOKEN = 1017;
  5351. const ONLY_AJAX_REQUESTS_ALLOWED = 1018;
  5352. const PAGINATION_FORBIDDEN = 1019;
  5353. private $values = [
  5354. 9999 => ["%s", ResponseFactory::INTERNAL_SERVER_ERROR],
  5355. 1000 => ["Route '%s' not found", ResponseFactory::NOT_FOUND],
  5356. 1001 => ["Table '%s' not found", ResponseFactory::NOT_FOUND],
  5357. 1002 => ["Argument count mismatch in '%s'", ResponseFactory::UNPROCESSABLE_ENTITY],
  5358. 1003 => ["Record '%s' not found", ResponseFactory::NOT_FOUND],
  5359. 1004 => ["Origin '%s' is forbidden", ResponseFactory::FORBIDDEN],
  5360. 1005 => ["Column '%s' not found", ResponseFactory::NOT_FOUND],
  5361. 1006 => ["Table '%s' already exists", ResponseFactory::CONFLICT],
  5362. 1007 => ["Column '%s' already exists", ResponseFactory::CONFLICT],
  5363. 1008 => ["Cannot read HTTP message", ResponseFactory::UNPROCESSABLE_ENTITY],
  5364. 1009 => ["Duplicate key exception", ResponseFactory::CONFLICT],
  5365. 1010 => ["Data integrity violation", ResponseFactory::CONFLICT],
  5366. 1011 => ["Authentication required", ResponseFactory::UNAUTHORIZED],
  5367. 1012 => ["Authentication failed for '%s'", ResponseFactory::FORBIDDEN],
  5368. 1013 => ["Input validation failed for '%s'", ResponseFactory::UNPROCESSABLE_ENTITY],
  5369. 1014 => ["Operation forbidden", ResponseFactory::FORBIDDEN],
  5370. 1015 => ["Operation '%s' not supported", ResponseFactory::METHOD_NOT_ALLOWED],
  5371. 1016 => ["Temporary or permanently blocked", ResponseFactory::FORBIDDEN],
  5372. 1017 => ["Bad or missing XSRF token", ResponseFactory::FORBIDDEN],
  5373. 1018 => ["Only AJAX requests allowed for '%s'", ResponseFactory::FORBIDDEN],
  5374. 1019 => ["Pagination forbidden", ResponseFactory::FORBIDDEN],
  5375. ];
  5376. public function __construct(int $code)
  5377. {
  5378. if (!isset($this->values[$code])) {
  5379. $code = 9999;
  5380. }
  5381. $this->code = $code;
  5382. $this->message = $this->values[$code][0];
  5383. $this->status = $this->values[$code][1];
  5384. }
  5385. public function getCode(): int
  5386. {
  5387. return $this->code;
  5388. }
  5389. public function getMessage(string $argument): string
  5390. {
  5391. return sprintf($this->message, $argument);
  5392. }
  5393. public function getStatus(): int
  5394. {
  5395. return $this->status;
  5396. }
  5397. }
  5398. // file: src/Tqdev/PhpCrudApi/Record/FilterInfo.php
  5399. class FilterInfo
  5400. {
  5401. private function addConditionFromFilterPath(PathTree $conditions, array $path, ReflectedTable $table, array $params)
  5402. {
  5403. $key = 'filter' . implode('', $path);
  5404. if (isset($params[$key])) {
  5405. foreach ($params[$key] as $filter) {
  5406. $condition = Condition::fromString($table, $filter);
  5407. if (($condition instanceof NoCondition) == false) {
  5408. $conditions->put($path, $condition);
  5409. }
  5410. }
  5411. }
  5412. }
  5413. private function getConditionsAsPathTree(ReflectedTable $table, array $params): PathTree
  5414. {
  5415. $conditions = new PathTree();
  5416. $this->addConditionFromFilterPath($conditions, [], $table, $params);
  5417. for ($n = ord('0'); $n <= ord('9'); $n++) {
  5418. $this->addConditionFromFilterPath($conditions, [chr($n)], $table, $params);
  5419. for ($l = ord('a'); $l <= ord('f'); $l++) {
  5420. $this->addConditionFromFilterPath($conditions, [chr($n), chr($l)], $table, $params);
  5421. }
  5422. }
  5423. return $conditions;
  5424. }
  5425. private function combinePathTreeOfConditions(PathTree $tree): Condition
  5426. {
  5427. $andConditions = $tree->getValues();
  5428. $and = AndCondition::fromArray($andConditions);
  5429. $orConditions = [];
  5430. foreach ($tree->getKeys() as $p) {
  5431. $orConditions[] = $this->combinePathTreeOfConditions($tree->get($p));
  5432. }
  5433. $or = OrCondition::fromArray($orConditions);
  5434. return $and->_and($or);
  5435. }
  5436. public function getCombinedConditions(ReflectedTable $table, array $params): Condition
  5437. {
  5438. return $this->combinePathTreeOfConditions($this->getConditionsAsPathTree($table, $params));
  5439. }
  5440. }
  5441. // file: src/Tqdev/PhpCrudApi/Record/HabtmValues.php
  5442. class HabtmValues
  5443. {
  5444. public $pkValues;
  5445. public $fkValues;
  5446. public function __construct(array $pkValues, array $fkValues)
  5447. {
  5448. $this->pkValues = $pkValues;
  5449. $this->fkValues = $fkValues;
  5450. }
  5451. }
  5452. // file: src/Tqdev/PhpCrudApi/Record/OrderingInfo.php
  5453. class OrderingInfo
  5454. {
  5455. public function getColumnOrdering(ReflectedTable $table, array $params): array
  5456. {
  5457. $fields = array();
  5458. if (isset($params['order'])) {
  5459. foreach ($params['order'] as $order) {
  5460. $parts = explode(',', $order, 3);
  5461. $columnName = $parts[0];
  5462. if (!$table->hasColumn($columnName)) {
  5463. continue;
  5464. }
  5465. $ascending = 'ASC';
  5466. if (count($parts) > 1) {
  5467. if (substr(strtoupper($parts[1]), 0, 4) == "DESC") {
  5468. $ascending = 'DESC';
  5469. }
  5470. }
  5471. $fields[] = [$columnName, $ascending];
  5472. }
  5473. }
  5474. if (count($fields) == 0) {
  5475. return $this->getDefaultColumnOrdering($table);
  5476. }
  5477. return $fields;
  5478. }
  5479. public function getDefaultColumnOrdering(ReflectedTable $table): array
  5480. {
  5481. $fields = array();
  5482. $pk = $table->getPk();
  5483. if ($pk) {
  5484. $fields[] = [$pk->getName(), 'ASC'];
  5485. } else {
  5486. foreach ($table->getColumnNames() as $columnName) {
  5487. $fields[] = [$columnName, 'ASC'];
  5488. }
  5489. }
  5490. return $fields;
  5491. }
  5492. }
  5493. // file: src/Tqdev/PhpCrudApi/Record/PaginationInfo.php
  5494. class PaginationInfo
  5495. {
  5496. public $DEFAULT_PAGE_SIZE = 20;
  5497. public function hasPage(array $params): bool
  5498. {
  5499. return isset($params['page']);
  5500. }
  5501. public function getPageOffset(array $params): int
  5502. {
  5503. $offset = 0;
  5504. $pageSize = $this->getPageSize($params);
  5505. if (isset($params['page'])) {
  5506. foreach ($params['page'] as $page) {
  5507. $parts = explode(',', $page, 2);
  5508. $page = intval($parts[0]) - 1;
  5509. $offset = $page * $pageSize;
  5510. }
  5511. }
  5512. return $offset;
  5513. }
  5514. private function getPageSize(array $params): int
  5515. {
  5516. $pageSize = $this->DEFAULT_PAGE_SIZE;
  5517. if (isset($params['page'])) {
  5518. foreach ($params['page'] as $page) {
  5519. $parts = explode(',', $page, 2);
  5520. if (count($parts) > 1) {
  5521. $pageSize = intval($parts[1]);
  5522. }
  5523. }
  5524. }
  5525. return $pageSize;
  5526. }
  5527. public function getResultSize(array $params): int
  5528. {
  5529. $numberOfRows = -1;
  5530. if (isset($params['size'])) {
  5531. foreach ($params['size'] as $size) {
  5532. $numberOfRows = intval($size);
  5533. }
  5534. }
  5535. return $numberOfRows;
  5536. }
  5537. public function getPageLimit(array $params): int
  5538. {
  5539. $pageLimit = -1;
  5540. if ($this->hasPage($params)) {
  5541. $pageLimit = $this->getPageSize($params);
  5542. }
  5543. $resultSize = $this->getResultSize($params);
  5544. if ($resultSize >= 0) {
  5545. if ($pageLimit >= 0) {
  5546. $pageLimit = min($pageLimit, $resultSize);
  5547. } else {
  5548. $pageLimit = $resultSize;
  5549. }
  5550. }
  5551. return $pageLimit;
  5552. }
  5553. }
  5554. // file: src/Tqdev/PhpCrudApi/Record/PathTree.php
  5555. class PathTree implements \JsonSerializable
  5556. {
  5557. const WILDCARD = '*';
  5558. private $tree;
  5559. public function __construct( /* object */&$tree = null)
  5560. {
  5561. if (!$tree) {
  5562. $tree = $this->newTree();
  5563. }
  5564. $this->tree = &$tree;
  5565. }
  5566. public function newTree()
  5567. {
  5568. return (object) ['values' => [], 'branches' => (object) []];
  5569. }
  5570. public function getKeys(): array
  5571. {
  5572. $branches = (array) $this->tree->branches;
  5573. return array_keys($branches);
  5574. }
  5575. public function getValues(): array
  5576. {
  5577. return $this->tree->values;
  5578. }
  5579. public function get(string $key): PathTree
  5580. {
  5581. if (!isset($this->tree->branches->$key)) {
  5582. return null;
  5583. }
  5584. return new PathTree($this->tree->branches->$key);
  5585. }
  5586. public function put(array $path, $value)
  5587. {
  5588. $tree = &$this->tree;
  5589. foreach ($path as $key) {
  5590. if (!isset($tree->branches->$key)) {
  5591. $tree->branches->$key = $this->newTree();
  5592. }
  5593. $tree = &$tree->branches->$key;
  5594. }
  5595. $tree->values[] = $value;
  5596. }
  5597. public function match(array $path): array
  5598. {
  5599. $star = self::WILDCARD;
  5600. $tree = &$this->tree;
  5601. foreach ($path as $key) {
  5602. if (isset($tree->branches->$key)) {
  5603. $tree = &$tree->branches->$key;
  5604. } else if (isset($tree->branches->$star)) {
  5605. $tree = &$tree->branches->$star;
  5606. } else {
  5607. return [];
  5608. }
  5609. }
  5610. return $tree->values;
  5611. }
  5612. public static function fromJson( /* object */$tree): PathTree
  5613. {
  5614. return new PathTree($tree);
  5615. }
  5616. public function jsonSerialize()
  5617. {
  5618. return $this->tree;
  5619. }
  5620. }
  5621. // file: src/Tqdev/PhpCrudApi/Record/RecordService.php
  5622. class RecordService
  5623. {
  5624. private $db;
  5625. private $reflection;
  5626. private $columns;
  5627. private $joiner;
  5628. private $filters;
  5629. private $ordering;
  5630. private $pagination;
  5631. public function __construct(GenericDB $db, ReflectionService $reflection)
  5632. {
  5633. $this->db = $db;
  5634. $this->reflection = $reflection;
  5635. $this->columns = new ColumnIncluder();
  5636. $this->joiner = new RelationJoiner($reflection, $this->columns);
  5637. $this->filters = new FilterInfo();
  5638. $this->ordering = new OrderingInfo();
  5639. $this->pagination = new PaginationInfo();
  5640. }
  5641. private function sanitizeRecord(string $tableName, /* object */ $record, string $id)
  5642. {
  5643. $keyset = array_keys((array) $record);
  5644. foreach ($keyset as $key) {
  5645. if (!$this->reflection->getTable($tableName)->hasColumn($key)) {
  5646. unset($record->$key);
  5647. }
  5648. }
  5649. if ($id != '') {
  5650. $pk = $this->reflection->getTable($tableName)->getPk();
  5651. foreach ($this->reflection->getTable($tableName)->getColumnNames() as $key) {
  5652. $field = $this->reflection->getTable($tableName)->getColumn($key);
  5653. if ($field->getName() == $pk->getName()) {
  5654. unset($record->$key);
  5655. }
  5656. }
  5657. }
  5658. }
  5659. public function hasTable(string $table): bool
  5660. {
  5661. return $this->reflection->hasTable($table);
  5662. }
  5663. public function getType(string $table): string
  5664. {
  5665. return $this->reflection->getType($table);
  5666. }
  5667. public function create(string $tableName, /* object */ $record, array $params) /*: ?int*/
  5668. {
  5669. $this->sanitizeRecord($tableName, $record, '');
  5670. $table = $this->reflection->getTable($tableName);
  5671. $columnValues = $this->columns->getValues($table, true, $record, $params);
  5672. return $this->db->createSingle($table, $columnValues);
  5673. }
  5674. public function read(string $tableName, string $id, array $params) /*: ?object*/
  5675. {
  5676. $table = $this->reflection->getTable($tableName);
  5677. $this->joiner->addMandatoryColumns($table, $params);
  5678. $columnNames = $this->columns->getNames($table, true, $params);
  5679. $record = $this->db->selectSingle($table, $columnNames, $id);
  5680. if ($record == null) {
  5681. return null;
  5682. }
  5683. $records = array($record);
  5684. $this->joiner->addJoins($table, $records, $params, $this->db);
  5685. return $records[0];
  5686. }
  5687. public function update(string $tableName, string $id, /* object */ $record, array $params) /*: ?int*/
  5688. {
  5689. $this->sanitizeRecord($tableName, $record, $id);
  5690. $table = $this->reflection->getTable($tableName);
  5691. $columnValues = $this->columns->getValues($table, true, $record, $params);
  5692. return $this->db->updateSingle($table, $columnValues, $id);
  5693. }
  5694. public function delete(string $tableName, string $id, array $params) /*: ?int*/
  5695. {
  5696. $table = $this->reflection->getTable($tableName);
  5697. return $this->db->deleteSingle($table, $id);
  5698. }
  5699. public function increment(string $tableName, string $id, /* object */ $record, array $params) /*: ?int*/
  5700. {
  5701. $this->sanitizeRecord($tableName, $record, $id);
  5702. $table = $this->reflection->getTable($tableName);
  5703. $columnValues = $this->columns->getValues($table, true, $record, $params);
  5704. return $this->db->incrementSingle($table, $columnValues, $id);
  5705. }
  5706. public function _list(string $tableName, array $params): ListDocument
  5707. {
  5708. $table = $this->reflection->getTable($tableName);
  5709. $this->joiner->addMandatoryColumns($table, $params);
  5710. $columnNames = $this->columns->getNames($table, true, $params);
  5711. $condition = $this->filters->getCombinedConditions($table, $params);
  5712. $columnOrdering = $this->ordering->getColumnOrdering($table, $params);
  5713. if (!$this->pagination->hasPage($params)) {
  5714. $offset = 0;
  5715. $limit = $this->pagination->getPageLimit($params);
  5716. $count = 0;
  5717. } else {
  5718. $offset = $this->pagination->getPageOffset($params);
  5719. $limit = $this->pagination->getPageLimit($params);
  5720. $count = $this->db->selectCount($table, $condition);
  5721. }
  5722. $records = $this->db->selectAll($table, $columnNames, $condition, $columnOrdering, $offset, $limit);
  5723. $this->joiner->addJoins($table, $records, $params, $this->db);
  5724. return new ListDocument($records, $count);
  5725. }
  5726. }
  5727. // file: src/Tqdev/PhpCrudApi/Record/RelationJoiner.php
  5728. class RelationJoiner
  5729. {
  5730. private $reflection;
  5731. private $columns;
  5732. public function __construct(ReflectionService $reflection, ColumnIncluder $columns)
  5733. {
  5734. $this->reflection = $reflection;
  5735. $this->ordering = new OrderingInfo();
  5736. $this->columns = $columns;
  5737. }
  5738. public function addMandatoryColumns(ReflectedTable $table, array &$params) /*: void*/
  5739. {
  5740. if (!isset($params['join']) || !isset($params['include'])) {
  5741. return;
  5742. }
  5743. $params['mandatory'] = array();
  5744. foreach ($params['join'] as $tableNames) {
  5745. $t1 = $table;
  5746. foreach (explode(',', $tableNames) as $tableName) {
  5747. if (!$this->reflection->hasTable($tableName)) {
  5748. continue;
  5749. }
  5750. $t2 = $this->reflection->getTable($tableName);
  5751. $fks1 = $t1->getFksTo($t2->getName());
  5752. $t3 = $this->hasAndBelongsToMany($t1, $t2);
  5753. if ($t3 != null || count($fks1) > 0) {
  5754. $params['mandatory'][] = $t2->getName() . '.' . $t2->getPk()->getName();
  5755. }
  5756. foreach ($fks1 as $fk) {
  5757. $params['mandatory'][] = $t1->getName() . '.' . $fk->getName();
  5758. }
  5759. $fks2 = $t2->getFksTo($t1->getName());
  5760. if ($t3 != null || count($fks2) > 0) {
  5761. $params['mandatory'][] = $t1->getName() . '.' . $t1->getPk()->getName();
  5762. }
  5763. foreach ($fks2 as $fk) {
  5764. $params['mandatory'][] = $t2->getName() . '.' . $fk->getName();
  5765. }
  5766. $t1 = $t2;
  5767. }
  5768. }
  5769. }
  5770. private function getJoinsAsPathTree(array $params): PathTree
  5771. {
  5772. $joins = new PathTree();
  5773. if (isset($params['join'])) {
  5774. foreach ($params['join'] as $tableNames) {
  5775. $path = array();
  5776. foreach (explode(',', $tableNames) as $tableName) {
  5777. $t = $this->reflection->getTable($tableName);
  5778. if ($t != null) {
  5779. $path[] = $t->getName();
  5780. }
  5781. }
  5782. $joins->put($path, true);
  5783. }
  5784. }
  5785. return $joins;
  5786. }
  5787. public function addJoins(ReflectedTable $table, array &$records, array $params, GenericDB $db) /*: void*/
  5788. {
  5789. $joins = $this->getJoinsAsPathTree($params);
  5790. $this->addJoinsForTables($table, $joins, $records, $params, $db);
  5791. }
  5792. private function hasAndBelongsToMany(ReflectedTable $t1, ReflectedTable $t2) /*: ?ReflectedTable*/
  5793. {
  5794. foreach ($this->reflection->getTableNames() as $tableName) {
  5795. $t3 = $this->reflection->getTable($tableName);
  5796. if (count($t3->getFksTo($t1->getName())) > 0 && count($t3->getFksTo($t2->getName())) > 0) {
  5797. return $t3;
  5798. }
  5799. }
  5800. return null;
  5801. }
  5802. private function addJoinsForTables(ReflectedTable $t1, PathTree $joins, array &$records, array $params, GenericDB $db)
  5803. {
  5804. foreach ($joins->getKeys() as $t2Name) {
  5805. $t2 = $this->reflection->getTable($t2Name);
  5806. $belongsTo = count($t1->getFksTo($t2->getName())) > 0;
  5807. $hasMany = count($t2->getFksTo($t1->getName())) > 0;
  5808. if (!$belongsTo && !$hasMany) {
  5809. $t3 = $this->hasAndBelongsToMany($t1, $t2);
  5810. } else {
  5811. $t3 = null;
  5812. }
  5813. $hasAndBelongsToMany = ($t3 != null);
  5814. $newRecords = array();
  5815. $fkValues = null;
  5816. $pkValues = null;
  5817. $habtmValues = null;
  5818. if ($belongsTo) {
  5819. $fkValues = $this->getFkEmptyValues($t1, $t2, $records);
  5820. $this->addFkRecords($t2, $fkValues, $params, $db, $newRecords);
  5821. }
  5822. if ($hasMany) {
  5823. $pkValues = $this->getPkEmptyValues($t1, $records);
  5824. $this->addPkRecords($t1, $t2, $pkValues, $params, $db, $newRecords);
  5825. }
  5826. if ($hasAndBelongsToMany) {
  5827. $habtmValues = $this->getHabtmEmptyValues($t1, $t2, $t3, $db, $records);
  5828. $this->addFkRecords($t2, $habtmValues->fkValues, $params, $db, $newRecords);
  5829. }
  5830. $this->addJoinsForTables($t2, $joins->get($t2Name), $newRecords, $params, $db);
  5831. if ($fkValues != null) {
  5832. $this->fillFkValues($t2, $newRecords, $fkValues);
  5833. $this->setFkValues($t1, $t2, $records, $fkValues);
  5834. }
  5835. if ($pkValues != null) {
  5836. $this->fillPkValues($t1, $t2, $newRecords, $pkValues);
  5837. $this->setPkValues($t1, $t2, $records, $pkValues);
  5838. }
  5839. if ($habtmValues != null) {
  5840. $this->fillFkValues($t2, $newRecords, $habtmValues->fkValues);
  5841. $this->setHabtmValues($t1, $t2, $records, $habtmValues);
  5842. }
  5843. }
  5844. }
  5845. private function getFkEmptyValues(ReflectedTable $t1, ReflectedTable $t2, array $records): array
  5846. {
  5847. $fkValues = array();
  5848. $fks = $t1->getFksTo($t2->getName());
  5849. foreach ($fks as $fk) {
  5850. $fkName = $fk->getName();
  5851. foreach ($records as $record) {
  5852. if (isset($record[$fkName])) {
  5853. $fkValue = $record[$fkName];
  5854. $fkValues[$fkValue] = null;
  5855. }
  5856. }
  5857. }
  5858. return $fkValues;
  5859. }
  5860. private function addFkRecords(ReflectedTable $t2, array $fkValues, array $params, GenericDB $db, array &$records) /*: void*/
  5861. {
  5862. $columnNames = $this->columns->getNames($t2, false, $params);
  5863. $fkIds = array_keys($fkValues);
  5864. foreach ($db->selectMultiple($t2, $columnNames, $fkIds) as $record) {
  5865. $records[] = $record;
  5866. }
  5867. }
  5868. private function fillFkValues(ReflectedTable $t2, array $fkRecords, array &$fkValues) /*: void*/
  5869. {
  5870. $pkName = $t2->getPk()->getName();
  5871. foreach ($fkRecords as $fkRecord) {
  5872. $pkValue = $fkRecord[$pkName];
  5873. $fkValues[$pkValue] = $fkRecord;
  5874. }
  5875. }
  5876. private function setFkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $fkValues) /*: void*/
  5877. {
  5878. $fks = $t1->getFksTo($t2->getName());
  5879. foreach ($fks as $fk) {
  5880. $fkName = $fk->getName();
  5881. foreach ($records as $i => $record) {
  5882. if (isset($record[$fkName])) {
  5883. $key = $record[$fkName];
  5884. $records[$i][$fkName] = $fkValues[$key];
  5885. }
  5886. }
  5887. }
  5888. }
  5889. private function getPkEmptyValues(ReflectedTable $t1, array $records): array
  5890. {
  5891. $pkValues = array();
  5892. $pkName = $t1->getPk()->getName();
  5893. foreach ($records as $record) {
  5894. $key = $record[$pkName];
  5895. $pkValues[$key] = array();
  5896. }
  5897. return $pkValues;
  5898. }
  5899. private function addPkRecords(ReflectedTable $t1, ReflectedTable $t2, array $pkValues, array $params, GenericDB $db, array &$records) /*: void*/
  5900. {
  5901. $fks = $t2->getFksTo($t1->getName());
  5902. $columnNames = $this->columns->getNames($t2, false, $params);
  5903. $pkValueKeys = implode(',', array_keys($pkValues));
  5904. $conditions = array();
  5905. foreach ($fks as $fk) {
  5906. $conditions[] = new ColumnCondition($fk, 'in', $pkValueKeys);
  5907. }
  5908. $condition = OrCondition::fromArray($conditions);
  5909. $columnOrdering = array();
  5910. $limit = VariableStore::get("joinLimits.maxRecords") ?: -1;
  5911. if ($limit != -1) {
  5912. $columnOrdering = $this->ordering->getDefaultColumnOrdering($t2);
  5913. }
  5914. foreach ($db->selectAll($t2, $columnNames, $condition, $columnOrdering, 0, $limit) as $record) {
  5915. $records[] = $record;
  5916. }
  5917. }
  5918. private function fillPkValues(ReflectedTable $t1, ReflectedTable $t2, array $pkRecords, array &$pkValues) /*: void*/
  5919. {
  5920. $fks = $t2->getFksTo($t1->getName());
  5921. foreach ($fks as $fk) {
  5922. $fkName = $fk->getName();
  5923. foreach ($pkRecords as $pkRecord) {
  5924. $key = $pkRecord[$fkName];
  5925. if (isset($pkValues[$key])) {
  5926. $pkValues[$key][] = $pkRecord;
  5927. }
  5928. }
  5929. }
  5930. }
  5931. private function setPkValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, array $pkValues) /*: void*/
  5932. {
  5933. $pkName = $t1->getPk()->getName();
  5934. $t2Name = $t2->getName();
  5935. foreach ($records as $i => $record) {
  5936. $key = $record[$pkName];
  5937. $records[$i][$t2Name] = $pkValues[$key];
  5938. }
  5939. }
  5940. private function getHabtmEmptyValues(ReflectedTable $t1, ReflectedTable $t2, ReflectedTable $t3, GenericDB $db, array $records): HabtmValues
  5941. {
  5942. $pkValues = $this->getPkEmptyValues($t1, $records);
  5943. $fkValues = array();
  5944. $fk1 = $t3->getFksTo($t1->getName())[0];
  5945. $fk2 = $t3->getFksTo($t2->getName())[0];
  5946. $fk1Name = $fk1->getName();
  5947. $fk2Name = $fk2->getName();
  5948. $columnNames = array($fk1Name, $fk2Name);
  5949. $pkIds = implode(',', array_keys($pkValues));
  5950. $condition = new ColumnCondition($t3->getColumn($fk1Name), 'in', $pkIds);
  5951. $columnOrdering = array();
  5952. $limit = VariableStore::get("joinLimits.maxRecords") ?: -1;
  5953. if ($limit != -1) {
  5954. $columnOrdering = $this->ordering->getDefaultColumnOrdering($t3);
  5955. }
  5956. $records = $db->selectAll($t3, $columnNames, $condition, $columnOrdering, 0, $limit);
  5957. foreach ($records as $record) {
  5958. $val1 = $record[$fk1Name];
  5959. $val2 = $record[$fk2Name];
  5960. $pkValues[$val1][] = $val2;
  5961. $fkValues[$val2] = null;
  5962. }
  5963. return new HabtmValues($pkValues, $fkValues);
  5964. }
  5965. private function setHabtmValues(ReflectedTable $t1, ReflectedTable $t2, array &$records, HabtmValues $habtmValues) /*: void*/
  5966. {
  5967. $pkName = $t1->getPk()->getName();
  5968. $t2Name = $t2->getName();
  5969. foreach ($records as $i => $record) {
  5970. $key = $record[$pkName];
  5971. $val = array();
  5972. $fks = $habtmValues->pkValues[$key];
  5973. foreach ($fks as $fk) {
  5974. $val[] = $habtmValues->fkValues[$fk];
  5975. }
  5976. $records[$i][$t2Name] = $val;
  5977. }
  5978. }
  5979. }
  5980. // file: src/Tqdev/PhpCrudApi/Api.php
  5981. class Api implements RequestHandlerInterface
  5982. {
  5983. private $router;
  5984. private $responder;
  5985. private $debug;
  5986. public function __construct(Config $config)
  5987. {
  5988. $db = new GenericDB(
  5989. $config->getDriver(),
  5990. $config->getAddress(),
  5991. $config->getPort(),
  5992. $config->getDatabase(),
  5993. $config->getUsername(),
  5994. $config->getPassword()
  5995. );
  5996. $cache = CacheFactory::create($config);
  5997. $reflection = new ReflectionService($db, $cache, $config->getCacheTime());
  5998. $responder = new Responder();
  5999. $router = new SimpleRouter($config->getBasePath(), $responder, $cache, $config->getCacheTime(), $config->getDebug());
  6000. foreach ($config->getMiddlewares() as $middleware => $properties) {
  6001. switch ($middleware) {
  6002. case 'cors':
  6003. new CorsMiddleware($router, $responder, $properties);
  6004. break;
  6005. case 'firewall':
  6006. new FirewallMiddleware($router, $responder, $properties);
  6007. break;
  6008. case 'basicAuth':
  6009. new BasicAuthMiddleware($router, $responder, $properties);
  6010. break;
  6011. case 'jwtAuth':
  6012. new JwtAuthMiddleware($router, $responder, $properties);
  6013. break;
  6014. case 'validation':
  6015. new ValidationMiddleware($router, $responder, $properties, $reflection);
  6016. break;
  6017. case 'ipAddress':
  6018. new IpAddressMiddleware($router, $responder, $properties, $reflection);
  6019. break;
  6020. case 'sanitation':
  6021. new SanitationMiddleware($router, $responder, $properties, $reflection);
  6022. break;
  6023. case 'multiTenancy':
  6024. new MultiTenancyMiddleware($router, $responder, $properties, $reflection);
  6025. break;
  6026. case 'authorization':
  6027. new AuthorizationMiddleware($router, $responder, $properties, $reflection);
  6028. break;
  6029. case 'xsrf':
  6030. new XsrfMiddleware($router, $responder, $properties);
  6031. break;
  6032. case 'pageLimits':
  6033. new PageLimitsMiddleware($router, $responder, $properties, $reflection);
  6034. break;
  6035. case 'joinLimits':
  6036. new JoinLimitsMiddleware($router, $responder, $properties, $reflection);
  6037. break;
  6038. case 'customization':
  6039. new CustomizationMiddleware($router, $responder, $properties, $reflection);
  6040. break;
  6041. }
  6042. }
  6043. foreach ($config->getControllers() as $controller) {
  6044. switch ($controller) {
  6045. case 'records':
  6046. $records = new RecordService($db, $reflection);
  6047. new RecordController($router, $responder, $records);
  6048. break;
  6049. case 'columns':
  6050. $definition = new DefinitionService($db, $reflection);
  6051. new ColumnController($router, $responder, $reflection, $definition);
  6052. break;
  6053. case 'cache':
  6054. new CacheController($router, $responder, $cache);
  6055. break;
  6056. case 'openapi':
  6057. $openApi = new OpenApiService($reflection, $config->getOpenApiBase());
  6058. new OpenApiController($router, $responder, $openApi);
  6059. break;
  6060. }
  6061. }
  6062. $this->router = $router;
  6063. $this->responder = $responder;
  6064. $this->debug = $config->getDebug();
  6065. }
  6066. public function handle(ServerRequestInterface $request): ResponseInterface
  6067. {
  6068. $response = null;
  6069. try {
  6070. $response = $this->router->route($request);
  6071. } catch (\Throwable $e) {
  6072. $response = $this->responder->error(ErrorCode::ERROR_NOT_FOUND, $e->getMessage());
  6073. if ($this->debug) {
  6074. $response = ResponseUtils::addExceptionHeaders($response, $e);
  6075. }
  6076. }
  6077. return $response;
  6078. }
  6079. }
  6080. // file: src/Tqdev/PhpCrudApi/Config.php
  6081. class Config
  6082. {
  6083. private $values = [
  6084. 'driver' => null,
  6085. 'address' => 'localhost',
  6086. 'port' => null,
  6087. 'username' => null,
  6088. 'password' => null,
  6089. 'database' => null,
  6090. 'middlewares' => 'cors',
  6091. 'controllers' => 'records,openapi',
  6092. 'cacheType' => 'TempFile',
  6093. 'cachePath' => '',
  6094. 'cacheTime' => 10,
  6095. 'debug' => false,
  6096. 'basePath' => '',
  6097. 'openApiBase' => '{"info":{"title":"PHP-CRUD-API","version":"1.0.0"}}',
  6098. ];
  6099. private function getDefaultDriver(array $values): string
  6100. {
  6101. if (isset($values['driver'])) {
  6102. return $values['driver'];
  6103. }
  6104. return 'mysql';
  6105. }
  6106. private function getDefaultPort(string $driver): int
  6107. {
  6108. switch ($driver) {
  6109. case 'mysql':return 3306;
  6110. case 'pgsql':return 5432;
  6111. case 'sqlsrv':return 1433;
  6112. }
  6113. }
  6114. private function getDefaultAddress(string $driver): string
  6115. {
  6116. switch ($driver) {
  6117. case 'mysql':return 'localhost';
  6118. case 'pgsql':return 'localhost';
  6119. case 'sqlsrv':return 'localhost';
  6120. }
  6121. }
  6122. private function getDriverDefaults(string $driver): array
  6123. {
  6124. return [
  6125. 'driver' => $driver,
  6126. 'address' => $this->getDefaultAddress($driver),
  6127. 'port' => $this->getDefaultPort($driver),
  6128. ];
  6129. }
  6130. public function __construct(array $values)
  6131. {
  6132. $driver = $this->getDefaultDriver($values);
  6133. $defaults = $this->getDriverDefaults($driver);
  6134. $newValues = array_merge($this->values, $defaults, $values);
  6135. $newValues = $this->parseMiddlewares($newValues);
  6136. $diff = array_diff_key($newValues, $this->values);
  6137. if (!empty($diff)) {
  6138. $key = array_keys($diff)[0];
  6139. throw new \Exception("Config has invalid value '$key'");
  6140. }
  6141. $this->values = $newValues;
  6142. }
  6143. private function parseMiddlewares(array $values): array
  6144. {
  6145. $newValues = array();
  6146. $properties = array();
  6147. $middlewares = array_map('trim', explode(',', $values['middlewares']));
  6148. foreach ($middlewares as $middleware) {
  6149. $properties[$middleware] = [];
  6150. }
  6151. foreach ($values as $key => $value) {
  6152. if (strpos($key, '.') === false) {
  6153. $newValues[$key] = $value;
  6154. } else {
  6155. list($middleware, $key2) = explode('.', $key, 2);
  6156. if (isset($properties[$middleware])) {
  6157. $properties[$middleware][$key2] = $value;
  6158. } else {
  6159. throw new \Exception("Config has invalid value '$key'");
  6160. }
  6161. }
  6162. }
  6163. $newValues['middlewares'] = array_reverse($properties, true);
  6164. return $newValues;
  6165. }
  6166. public function getDriver(): string
  6167. {
  6168. return $this->values['driver'];
  6169. }
  6170. public function getAddress(): string
  6171. {
  6172. return $this->values['address'];
  6173. }
  6174. public function getPort(): int
  6175. {
  6176. return $this->values['port'];
  6177. }
  6178. public function getUsername(): string
  6179. {
  6180. return $this->values['username'];
  6181. }
  6182. public function getPassword(): string
  6183. {
  6184. return $this->values['password'];
  6185. }
  6186. public function getDatabase(): string
  6187. {
  6188. return $this->values['database'];
  6189. }
  6190. public function getMiddlewares(): array
  6191. {
  6192. return $this->values['middlewares'];
  6193. }
  6194. public function getControllers(): array
  6195. {
  6196. return array_map('trim', explode(',', $this->values['controllers']));
  6197. }
  6198. public function getCacheType(): string
  6199. {
  6200. return $this->values['cacheType'];
  6201. }
  6202. public function getCachePath(): string
  6203. {
  6204. return $this->values['cachePath'];
  6205. }
  6206. public function getCacheTime(): int
  6207. {
  6208. return $this->values['cacheTime'];
  6209. }
  6210. public function getDebug(): bool
  6211. {
  6212. return $this->values['debug'];
  6213. }
  6214. public function getBasePath(): string
  6215. {
  6216. return $this->values['basePath'];
  6217. }
  6218. public function getOpenApiBase(): array
  6219. {
  6220. return json_decode($this->values['openApiBase'], true);
  6221. }
  6222. }
  6223. // file: src/Tqdev/PhpCrudApi/RequestFactory.php
  6224. class RequestFactory
  6225. {
  6226. private static function parseBody(string $body) /*: ?object*/
  6227. {
  6228. $first = substr($body, 0, 1);
  6229. if ($first == '[' || $first == '{') {
  6230. $object = json_decode($body);
  6231. $causeCode = json_last_error();
  6232. if ($causeCode !== JSON_ERROR_NONE) {
  6233. $object = null;
  6234. }
  6235. } else {
  6236. parse_str($body, $input);
  6237. foreach ($input as $key => $value) {
  6238. if (substr($key, -9) == '__is_null') {
  6239. $input[substr($key, 0, -9)] = null;
  6240. unset($input[$key]);
  6241. }
  6242. }
  6243. $object = (object) $input;
  6244. }
  6245. return $object;
  6246. }
  6247. public static function fromGlobals(): ServerRequestInterface
  6248. {
  6249. $psr17Factory = new Psr17Factory();
  6250. $creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
  6251. $serverRequest = $creator->fromGlobals();
  6252. $body = file_get_contents('php://input');
  6253. if ($body) {
  6254. $serverRequest = $serverRequest->withParsedBody(self::parseBody($body));
  6255. }
  6256. return $serverRequest;
  6257. }
  6258. public static function fromString(string $request): ServerRequestInterface
  6259. {
  6260. $parts = explode("\n\n", trim($request), 2);
  6261. $lines = explode("\n", $parts[0]);
  6262. $first = explode(' ', trim(array_shift($lines)), 2);
  6263. $method = $first[0];
  6264. $body = isset($parts[1]) ? $parts[1] : '';
  6265. $url = isset($first[1]) ? $first[1] : '';
  6266. $psr17Factory = new Psr17Factory();
  6267. $serverRequest = $psr17Factory->createServerRequest($method, $url);
  6268. foreach ($lines as $line) {
  6269. list($key, $value) = explode(':', $line, 2);
  6270. $serverRequest = $serverRequest->withAddedHeader($key, $value);
  6271. }
  6272. if ($body) {
  6273. $stream = $psr17Factory->createStream($body);
  6274. $stream->rewind();
  6275. $serverRequest = $serverRequest->withBody($stream);
  6276. $serverRequest = $serverRequest->withParsedBody(self::parseBody($body));
  6277. }
  6278. return $serverRequest;
  6279. }
  6280. }
  6281. // file: src/Tqdev/PhpCrudApi/RequestUtils.php
  6282. class RequestUtils
  6283. {
  6284. public static function setParams(ServerRequestInterface $request, array $params): ServerRequestInterface
  6285. {
  6286. $query = preg_replace('|%5B[0-9]+%5D=|', '=', http_build_query($params));
  6287. return $request->withUri($request->getUri()->withQuery($query));
  6288. }
  6289. public static function getHeader(ServerRequestInterface $request, string $header): string
  6290. {
  6291. $headers = $request->getHeader($header);
  6292. return isset($headers[0]) ? $headers[0] : '';
  6293. }
  6294. public static function getParams(ServerRequestInterface $request): array
  6295. {
  6296. $params = array();
  6297. $query = $request->getUri()->getQuery();
  6298. $query = str_replace('][]=', ']=', str_replace('=', '[]=', $query));
  6299. parse_str($query, $params);
  6300. return $params;
  6301. }
  6302. public static function getPathSegment(ServerRequestInterface $request, int $part): string
  6303. {
  6304. $path = $request->getUri()->getPath();
  6305. $pathSegments = explode('/', rtrim($path, '/'));
  6306. if ($part < 0 || $part >= count($pathSegments)) {
  6307. return '';
  6308. }
  6309. return urldecode($pathSegments[$part]);
  6310. }
  6311. public static function getOperation(ServerRequestInterface $request): string
  6312. {
  6313. $method = $request->getMethod();
  6314. $path = RequestUtils::getPathSegment($request, 1);
  6315. $hasPk = RequestUtils::getPathSegment($request, 3) != '';
  6316. switch ($path) {
  6317. case 'openapi':
  6318. return 'document';
  6319. case 'columns':
  6320. return $method == 'get' ? 'reflect' : 'remodel';
  6321. case 'records':
  6322. switch ($method) {
  6323. case 'POST':
  6324. return 'create';
  6325. case 'GET':
  6326. return $hasPk ? 'read' : 'list';
  6327. case 'PUT':
  6328. return 'update';
  6329. case 'DELETE':
  6330. return 'delete';
  6331. case 'PATCH':
  6332. return 'increment';
  6333. }
  6334. }
  6335. return 'unknown';
  6336. }
  6337. private static function getJoinTables(string $tableName, array $parameters): array
  6338. {
  6339. $uniqueTableNames = array();
  6340. $uniqueTableNames[$tableName] = true;
  6341. if (isset($parameters['join'])) {
  6342. foreach ($parameters['join'] as $parameter) {
  6343. $tableNames = explode(',', trim($parameter));
  6344. foreach ($tableNames as $tableName) {
  6345. $uniqueTableNames[$tableName] = true;
  6346. }
  6347. }
  6348. }
  6349. return array_keys($uniqueTableNames);
  6350. }
  6351. public static function getTableNames(ServerRequestInterface $request, ReflectionService $reflection): array
  6352. {
  6353. $path = RequestUtils::getPathSegment($request, 1);
  6354. $tableName = RequestUtils::getPathSegment($request, 2);
  6355. $allTableNames = $reflection->getTableNames();
  6356. switch ($path) {
  6357. case 'openapi':
  6358. return $allTableNames;
  6359. case 'columns':
  6360. return $tableName ? [$tableName] : $allTableNames;
  6361. case 'records':
  6362. return self::getJoinTables($tableName, RequestUtils::getParams($request));
  6363. }
  6364. return $allTableNames;
  6365. }
  6366. }
  6367. // file: src/Tqdev/PhpCrudApi/ResponseFactory.php
  6368. class ResponseFactory
  6369. {
  6370. const OK = 200;
  6371. const UNAUTHORIZED = 401;
  6372. const FORBIDDEN = 403;
  6373. const NOT_FOUND = 404;
  6374. const METHOD_NOT_ALLOWED = 405;
  6375. const CONFLICT = 409;
  6376. const UNPROCESSABLE_ENTITY = 422;
  6377. const INTERNAL_SERVER_ERROR = 500;
  6378. public static function fromObject(int $status, $body): ResponseInterface
  6379. {
  6380. $psr17Factory = new Psr17Factory();
  6381. $response = $psr17Factory->createResponse($status);
  6382. $content = json_encode($body, JSON_UNESCAPED_UNICODE);
  6383. $stream = $psr17Factory->createStream($content);
  6384. $stream->rewind();
  6385. $response = $response->withBody($stream);
  6386. $response = $response->withHeader('Content-Type', 'application/json');
  6387. $response = $response->withHeader('Content-Length', strlen($content));
  6388. return $response;
  6389. }
  6390. public static function fromStatus(int $status): ResponseInterface
  6391. {
  6392. $psr17Factory = new Psr17Factory();
  6393. return $psr17Factory->createResponse($status);
  6394. }
  6395. }
  6396. // file: src/Tqdev/PhpCrudApi/ResponseUtils.php
  6397. class ResponseUtils
  6398. {
  6399. public static function output(ResponseInterface $response)
  6400. {
  6401. $status = $response->getStatusCode();
  6402. $headers = $response->getHeaders();
  6403. $body = $response->getBody()->getContents();
  6404. http_response_code($status);
  6405. foreach ($headers as $key => $values) {
  6406. foreach ($values as $value) {
  6407. header("$key: $value");
  6408. }
  6409. }
  6410. echo $body;
  6411. }
  6412. public static function addExceptionHeaders(ResponseInterface $response, \Throwable $e): ResponseInterface
  6413. {
  6414. $response = $response->withHeader('X-Exception-Name', get_class($e));
  6415. $response = $response->withHeader('X-Exception-Message', $e->getMessage());
  6416. $response = $response->withHeader('X-Exception-File', $e->getFile() . ':' . $e->getLine());
  6417. return $response;
  6418. }
  6419. public static function toString(ResponseInterface $response): string
  6420. {
  6421. $status = $response->getStatusCode();
  6422. $headers = $response->getHeaders();
  6423. $body = $response->getBody()->getContents();
  6424. $str = "$status\n";
  6425. foreach ($headers as $key => $values) {
  6426. foreach ($values as $value) {
  6427. $str .= "$key: $value\n";
  6428. }
  6429. }
  6430. if ($body !== '') {
  6431. $str .= "\n";
  6432. $str .= "$body\n";
  6433. }
  6434. return $str;
  6435. }
  6436. }
  6437. // file: src/index.php
  6438. $config = new Config([
  6439. 'username' => 'php-crud-api',
  6440. 'password' => 'php-crud-api',
  6441. 'database' => 'php-crud-api',
  6442. ]);
  6443. $request = RequestFactory::fromGlobals();
  6444. $api = new Api($config);
  6445. $response = $api->handle($request);
  6446. ResponseUtils::output($response);